├── README.md
├── client.go
├── constants.go
├── docs.go
├── example
├── client.go
└── server.go
├── filesystem.go
├── lock.go
├── node.go
├── node_test.go
├── server.go
└── util.go
/README.md:
--------------------------------------------------------------------------------
1 | # webdav
2 |
3 | __TODO (litmus tests):__
4 | * basic..... pass
5 | * copymove.. pass
6 | * props..... FAILED
7 | * init.................. pass
8 | * begin................. pass
9 | * propfind_invalid...... pass
10 | * propfind_invalid2..... pass
11 | * propfind_d0........... pass
12 | * propinit.............. pass
13 | * propset............... FAIL
14 | * propget............... SKIPPED
15 | * propextended.......... pass
16 | * propmove.............. SKIPPED
17 | * propget............... SKIPPED
18 | * propdeletes........... SKIPPED
19 | * propget............... SKIPPED
20 | * propreplace........... SKIPPED
21 | * propget............... SKIPPED
22 | * propnullns............ SKIPPED
23 | * propget............... SKIPPED
24 | * prophighunicode....... SKIPPED
25 | * propget............... SKIPPED
26 | * propremoveset......... SKIPPED
27 | * propget............... SKIPPED
28 | * propsetremove......... SKIPPED
29 | * propget............... SKIPPED
30 | * propvalnspace......... SKIPPED
31 | * propwformed........... pass
32 | * propinit.............. pass
33 | * propmanyns............ FAIL
34 | * propget............... FAIL
35 | * propcleanup........... pass
36 | * finish................ pass
37 | * locks..... SKIPPED
38 | * http...... SKIPPED
39 |
40 | __future:__
41 | * server
42 | * client
43 | * replace litmus test with plain go tests
44 |
--------------------------------------------------------------------------------
/client.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import ()
4 |
5 | type FileSystemCloser interface {
6 | FileSystem
7 | Close() error
8 | }
9 |
10 | func Dial(url string) (FileSystemCloser, error) {
11 | // TODO
12 | return nil, ErrNotImplemented
13 | }
14 |
--------------------------------------------------------------------------------
/constants.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "errors"
5 | "net/http"
6 | )
7 |
8 | // status codes
9 | const (
10 | StatusOK = http.StatusOK
11 | StatusCreated = http.StatusCreated
12 | StatusAccepted = http.StatusAccepted
13 | StatusNoContent = http.StatusNoContent
14 | StatusMovedPermanently = http.StatusMovedPermanently
15 | StatusMovedTemporarily = 302 // TODO: duplicate of http.StatusFound ?
16 | StatusNotModified = http.StatusNotModified
17 | StatusBadRequest = http.StatusBadRequest
18 | StatusUnauthorized = http.StatusUnauthorized
19 | StatusForbidden = http.StatusForbidden
20 | StatusNotFound = http.StatusNotFound
21 | StatusInternalServerError = http.StatusInternalServerError
22 | StatusNotImplemented = http.StatusNotImplemented
23 | StatusBadGateway = http.StatusBadGateway
24 | StatusServiceUnavailable = http.StatusServiceUnavailable
25 | StatusContinue = http.StatusContinue
26 | StatusMethodNotAllowed = http.StatusMethodNotAllowed
27 | StatusConflict = http.StatusConflict
28 | StatusPreconditionFailed = http.StatusPreconditionFailed
29 | StatusRequestTooLong = http.StatusRequestEntityTooLarge
30 | StatusUnsupportedMediaType = http.StatusUnsupportedMediaType
31 | )
32 |
33 | // extended status codes, http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
34 | const (
35 | StatusMulti = 207
36 | StatusUnprocessableEntity = 422
37 | StatusLocked = 423
38 | StatusFailedDependency = 424
39 | StatusInsufficientStorage = 507
40 | )
41 |
42 | var statusText = map[int]string{
43 | StatusMovedTemporarily: "Moved Temporarily",
44 | StatusMulti: "Multi-Status",
45 | StatusUnprocessableEntity: "Unprocessable Entity",
46 | StatusLocked: "Locked",
47 | StatusFailedDependency: "Failed Dependency",
48 | StatusInsufficientStorage: "Insufficient Storage",
49 | }
50 |
51 | // StatusText returns a text for the HTTP status code. It returns the empty string if the code is unknown.
52 | func StatusText(code int) string {
53 | if t, ok := statusText[code]; ok {
54 | return t
55 | }
56 |
57 | return http.StatusText(code)
58 | }
59 |
60 | // internal error variables
61 | var (
62 | ErrInvalidCharPath = errors.New("invalid character in file path")
63 | ErrNotImplemented = errors.New("feature not yet implemented")
64 | ErrMalformedXml = errors.New("xml is not well-formed")
65 | )
66 |
--------------------------------------------------------------------------------
/docs.go:
--------------------------------------------------------------------------------
1 | /*
2 | package webdav provides WEBDAV client and server implementations
3 |
4 | webdav java
5 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/org/apache/catalina/servlets/WebdavServlet.java
6 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/org/apache/catalina/servlets/DefaultServlet.java
7 | http://svn.apache.org/repos/asf/tomcat/tc7.0.x/trunk/java/javax/servlet/http/HttpServlet.java
8 |
9 | webdav go partially
10 | https://github.com/bradfitz/camlistore/blob/master/old/camwebdav/main.go
11 | https://github.com/adrianuswarmenhoven/XBMC-BlastDav/blob/master/main.go
12 |
13 | go http extension examples
14 | http://godoc.org/net/http#_example_FileServer--StripPrefix
15 | http://godoc.org/code.google.com/p/go.net/websocket
16 |
17 | */
18 | package webdav
19 |
--------------------------------------------------------------------------------
/example/client.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "log"
5 |
6 | "github.com/der-antikeks/go-webdav"
7 | )
8 |
9 | func main() {
10 | fs, err := webdav.Dial("http://localhost:8080/webdav")
11 | if err != nil {
12 | log.Fatal(err)
13 | }
14 | defer fs.Close()
15 |
16 | f, err := fs.Open(".")
17 | if err != nil {
18 | log.Fatal(err)
19 | }
20 | defer f.Close()
21 |
22 | fi, err := f.Readdir(0)
23 | if err != nil {
24 | log.Fatal(err)
25 | }
26 |
27 | for _, i := range fi {
28 | name := i.Name()
29 | if i.IsDir() {
30 | name += "/"
31 | }
32 |
33 | log.Println(name)
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/example/server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "log"
6 | "net/http"
7 | "os"
8 |
9 | "github.com/der-antikeks/go-webdav"
10 | )
11 |
12 | var (
13 | path = "./tmp"
14 | )
15 |
16 | func main() {
17 | os.Mkdir(path, os.ModeDir)
18 |
19 | // http.StripPrefix is not working, webdav.Server has no knowledge
20 | // of stripped component, but needs for COPY/MOVE methods.
21 | // Destination path is supplied as header and needs to be stripped.
22 | http.Handle("/webdav/", &webdav.Server{
23 | Fs: webdav.Dir(path),
24 | TrimPrefix: "/webdav/",
25 | Listings: true,
26 | })
27 |
28 | http.HandleFunc("/", index)
29 |
30 | log.Println("Listening on http://127.0.0.1:8080")
31 | log.Fatal(http.ListenAndServe(":8080", nil))
32 | }
33 |
34 | func index(w http.ResponseWriter, r *http.Request) {
35 | fmt.Fprintf(w, "Hello, %q\n", r.URL.Path)
36 | }
37 |
--------------------------------------------------------------------------------
/filesystem.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "io"
5 | "os"
6 | "path"
7 | "path/filepath"
8 | "strings"
9 | )
10 |
11 | // A FileSystem implements access to a collection of named files.
12 | // The elements in a file path are separated by slash ('/', U+002F)
13 | // characters, regardless of host operating system convention.
14 | type FileSystem interface {
15 | Open(name string) (File, error)
16 | Create(name string) (File, error)
17 | Mkdir(path string) error
18 | Remove(name string) error
19 | }
20 |
21 | // A File is returned by a FileSystem's Open and Create method and can
22 | // be served by the FileServer implementation.
23 | type File interface {
24 | Stat() (os.FileInfo, error)
25 | Readdir(count int) ([]os.FileInfo, error)
26 |
27 | Read([]byte) (int, error)
28 | Write(p []byte) (n int, err error)
29 | Seek(offset int64, whence int) (int64, error)
30 | Close() error
31 |
32 | /* TODO: needed?
33 | Chdir() error
34 | Chmod(mode FileMode) error
35 | Chown(uid, gid int) error
36 | */
37 | }
38 |
39 | // A Dir implements webdav.FileSystem using the native file
40 | // system restricted to a specific directory tree.
41 | //
42 | // An empty Dir is treated as ".".
43 | type Dir string
44 |
45 | func (d Dir) sanitizePath(name string) (string, error) {
46 | if filepath.Separator != '/' && strings.IndexRune(name, filepath.Separator) >= 0 ||
47 | strings.Contains(name, "\x00") {
48 | return "", ErrInvalidCharPath
49 | }
50 |
51 | dir := string(d)
52 | if dir == "" {
53 | dir = "."
54 | }
55 |
56 | return filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))), nil
57 | }
58 |
59 | func (d Dir) Open(name string) (File, error) {
60 | p, err := d.sanitizePath(name)
61 | if err != nil {
62 | return nil, err
63 | }
64 |
65 | f, err := os.Open(p)
66 | if err != nil {
67 | return nil, err
68 | }
69 | return f, nil
70 | }
71 |
72 | func (d Dir) Create(name string) (File, error) {
73 | p, err := d.sanitizePath(name)
74 | if err != nil {
75 | return nil, err
76 | }
77 |
78 | f, err := os.Create(p)
79 | if err != nil {
80 | return nil, err
81 | }
82 | return f, nil
83 | }
84 |
85 | // Mkdir creates a new directory with the specified name
86 | func (d Dir) Mkdir(name string) error {
87 | p, err := d.sanitizePath(name)
88 | if err != nil {
89 | return err
90 | }
91 |
92 | return os.Mkdir(p, os.ModePerm)
93 | }
94 |
95 | func (d Dir) Remove(name string) error {
96 | p, err := d.sanitizePath(name)
97 | if err != nil {
98 | return err
99 | }
100 |
101 | return os.Remove(p)
102 | }
103 |
104 | // mockup zero content file aka only header
105 | type emptyFile struct{}
106 |
107 | func (e emptyFile) Read(p []byte) (n int, err error) {
108 | return 0, io.EOF
109 | }
110 |
111 | func (e emptyFile) Seek(offset int64, whence int) (ret int64, err error) {
112 | return 0, nil
113 | }
114 |
--------------------------------------------------------------------------------
/lock.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "errors"
5 | "fmt"
6 | "strings"
7 | "time"
8 | )
9 |
10 | type Lock struct {
11 | uri string
12 | creator string
13 | owner string
14 | depth int
15 | timeout TimeOut
16 | typ string
17 | scope string
18 | token string
19 | Modified time.Time
20 | }
21 |
22 | func NewLock(uri, creator, owner string) *Lock {
23 | return &Lock{
24 | uri,
25 | creator,
26 | owner,
27 | 0,
28 | 0,
29 | "write",
30 | "exclusive",
31 | generateToken(),
32 | time.Now(),
33 | }
34 | }
35 |
36 | // parse a lock from a http request
37 | func ParseLockString(body string) (*Lock, error) {
38 | node, err := NodeFromXmlString(body)
39 | if err != nil {
40 | return nil, err
41 | }
42 |
43 | if node == nil {
44 | return nil, errors.New("no found node")
45 | }
46 |
47 | lock := new(Lock)
48 |
49 | if node.Name.Local != "lockinfo" {
50 | node = node.FirstChildren("lockinfo")
51 | }
52 | if node == nil {
53 | return nil, errors.New("not lockinfo element")
54 | }
55 |
56 | lock.scope = node.FirstChildren("lockscope").Children[0].Name.Local
57 |
58 | lock.typ = node.FirstChildren("locktype").Children[0].Name.Local
59 |
60 | lock.owner = node.FirstChildren("owner").Children[0].Value
61 |
62 | return lock, nil
63 | }
64 |
65 | func (lock *Lock) Refresh() {
66 | lock.Modified = time.Now()
67 | }
68 |
69 | func (lock *Lock) IsValid() bool {
70 | return time.Duration(lock.timeout) > time.Now().Sub(lock.Modified)
71 | }
72 |
73 | func (lock *Lock) GetTimeout() TimeOut {
74 | return lock.timeout
75 | }
76 |
77 | func (lock *Lock) SetTimeout(timeout time.Duration) {
78 | lock.timeout = TimeOut(timeout)
79 | lock.Modified = time.Now()
80 | }
81 |
82 | func (lock *Lock) asXML(namespace string, discover bool) string {
83 | //owner_str = lock.owner
84 | //owner_str = "".join([node.toxml() for node in self.owner[0].childNodes])
85 |
86 | base := fmt.Sprintf(`<%[1]s:activelock>
87 | <%[1]s:locktype><%[1]s:%[2]s/>%[1]s:locktype>
88 | <%[1]s:lockscope><%[1]s:%[3]s/>%[1]s:lockscope>
89 | <%[1]s:depth>%[4]d%[1]s:depth>
90 | <%[1]s:owner>%[5]s%[1]s:owner>
91 | <%[1]s:timeout>%[6]s%[1]s:timeout>
92 | <%[1]s:locktoken>
93 | <%[1]s:href>opaquelocktoken:%[7]s%[1]s:href>
94 | %[1]s:locktoken>
95 | <%[1]s:lockroot>
96 | <%[1]s:href>%[8]s%[1]s:href>
97 | %[1]s:lockroot>
98 | %[1]s:activelock>
99 | `, strings.Trim(namespace, ":"),
100 | lock.typ,
101 | lock.scope,
102 | lock.depth,
103 | lock.owner,
104 | lock.GetTimeout(),
105 | lock.token,
106 | lock.uri,
107 | )
108 |
109 | if discover {
110 | return base
111 | }
112 |
113 | return fmt.Sprintf(`
114 |
115 |
116 | %s
117 |
118 | `, base)
119 | }
120 |
--------------------------------------------------------------------------------
/node.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "encoding/xml"
5 | "io"
6 | "strings"
7 | )
8 |
9 | type Node struct {
10 | Name xml.Name
11 | Attr []xml.Attr
12 | Children []*Node
13 | Value string
14 | Parent *Node
15 | }
16 |
17 | func NodeFromXmlString(xmls string) (*Node, error) {
18 | rd := strings.NewReader(xmls)
19 | return NodeFromXml(rd)
20 | }
21 |
22 | func NodeFromXml(r io.Reader) (*Node, error) {
23 | var cur, parent *Node
24 |
25 | decoder := xml.NewDecoder(r)
26 | for {
27 | token, err := decoder.Token()
28 | if err != nil {
29 | if err != io.EOF {
30 | return nil, err
31 | }
32 | break
33 | }
34 |
35 | switch tok := token.(type) {
36 | case xml.StartElement:
37 | parent = cur
38 |
39 | // if tok.Name.Space != "DAV:" {
40 | if tok.Name.Space == "" {
41 | return nil, ErrMalformedXml
42 | }
43 |
44 | cur = &Node{
45 | Name: tok.Name,
46 | Attr: tok.Attr,
47 | Parent: parent,
48 | }
49 |
50 | if parent != nil {
51 | parent.Children = append(parent.Children, cur)
52 | }
53 | case xml.CharData:
54 | if cur != nil {
55 | cur.Value = string(tok)
56 | }
57 | case xml.EndElement:
58 | if cur.Parent == nil {
59 | return cur, nil
60 | }
61 | cur = cur.Parent
62 | default:
63 | //log.Printf("%T", tok)
64 | }
65 | }
66 |
67 | return cur, nil
68 | }
69 |
70 | func (n Node) HasChildren(name string) bool {
71 | for _, v := range n.Children {
72 | if v.Name.Local == name {
73 | return true
74 | }
75 | }
76 | return false
77 | }
78 |
79 | func (n *Node) GetChildrens(name string) []*Node {
80 | if name == "*" {
81 | return n.Children
82 | }
83 |
84 | var ret []*Node
85 |
86 | for _, v := range n.Children {
87 | if v.Name.Local == name {
88 | ret = append(ret, v)
89 | }
90 | }
91 |
92 | return ret
93 | }
94 |
95 | func (n *Node) FirstChildren(name string) *Node {
96 | if name == "*" && len(n.Children) > 0 {
97 | return n.Children[0]
98 | }
99 |
100 | for _, v := range n.Children {
101 | if v.Name.Local == name {
102 | return v
103 | }
104 | }
105 |
106 | return nil
107 | }
108 |
109 | func (n *Node) String() string {
110 | r := "<" + n.Name.Local + " xmlns=\"" + n.Name.Space + "\">\n"
111 |
112 | if len(n.Children) > 0 {
113 | for _, v := range n.Children {
114 | r += v.String() + "\n"
115 | }
116 | } else if len(n.Value) > 0 {
117 | r += n.Value + "\n"
118 | }
119 |
120 | r += "<\\" + n.Name.Local + ">"
121 |
122 | return r
123 | }
124 |
--------------------------------------------------------------------------------
/node_test.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "fmt"
5 | "strings"
6 | "testing"
7 | )
8 |
9 | var txt = `
10 |
11 |
12 |
13 |
14 | mailto:xiaolunwen@gmail.com
15 |
16 | `
17 |
18 | func TestNodeFromXml(t *testing.T) {
19 | rd := strings.NewReader(txt)
20 | node, err := NodeFromXml(rd)
21 | if err != nil {
22 | t.Error(err)
23 | }
24 |
25 | fmt.Println("node.Name.Local", node.Name.Local)
26 | fmt.Println("node.Name.Space", node.Name.Space)
27 | fmt.Println(node.FirstChildren("owner"))
28 | }
29 |
--------------------------------------------------------------------------------
/server.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io"
7 | "io/ioutil"
8 | "math/rand"
9 | "mime"
10 | "net/http"
11 | "net/url"
12 | "os"
13 | "os/exec"
14 | "path"
15 | "path/filepath"
16 | "strconv"
17 | "strings"
18 | "time"
19 |
20 | qlog "github.com/qiniu/log"
21 | )
22 |
23 | var logger *qlog.Logger
24 |
25 | func newLogger(execDir string) {
26 | logPath := execDir + "/log/webdav.log"
27 | os.MkdirAll(path.Dir(logPath), os.ModePerm)
28 |
29 | f, err := os.OpenFile(logPath, os.O_WRONLY|os.O_APPEND|os.O_CREATE, os.ModePerm)
30 | if err != nil {
31 | qlog.Fatal(err)
32 | }
33 |
34 | logger = qlog.New(f, "", qlog.Ldate|qlog.Ltime)
35 | logger.Info("Start logging webdav...")
36 | }
37 |
38 | func ExecDir() (string, error) {
39 | file, err := exec.LookPath(os.Args[0])
40 | if err != nil {
41 | return "", err
42 | }
43 | p, err := filepath.Abs(file)
44 | if err != nil {
45 | return "", err
46 | }
47 | return path.Dir(strings.Replace(p, "\\", "/", -1)), nil
48 | }
49 |
50 | func init() {
51 | exePath, err := ExecDir()
52 | if err != nil {
53 | panic(err)
54 | }
55 |
56 | newLogger(exePath)
57 | }
58 |
59 | func Handler(root FileSystem) http.Handler {
60 | return &Server{Fs: root}
61 | }
62 |
63 | type Server struct {
64 | // trimmed path prefix
65 | TrimPrefix string
66 |
67 | // files are readonly?
68 | ReadOnly bool
69 |
70 | // generate directory listings?
71 | Listings bool
72 |
73 | // access to a collection of named files
74 | Fs FileSystem
75 |
76 | tokens_to_lock map[string]*Lock
77 |
78 | path_to_token map[string]string
79 | }
80 |
81 | func generateToken() string {
82 | r := rand.New(rand.NewSource(time.Now().UnixNano()))
83 | return fmt.Sprintf("%d-%d-00105A989226:%d",
84 | r.Int31(), r.Int31(), time.Now().UnixNano())
85 | }
86 |
87 | func NewServer(dir, prefix string, listDir bool) *Server {
88 | return &Server{
89 | Fs: Dir(dir),
90 | TrimPrefix: prefix,
91 | Listings: listDir,
92 | tokens_to_lock: make(map[string]*Lock),
93 | path_to_token: make(map[string]string),
94 | }
95 | }
96 |
97 | var (
98 | PullMethods = map[string]bool{
99 | "OPTIONS": true,
100 | "GET": true,
101 | "HEAD": true,
102 | "PROPFIND": true}
103 |
104 | PushMethods = map[string]bool{
105 | "POST": true,
106 | "DELETE": true,
107 | "PUT": true,
108 | "PROPPATCH": true,
109 | "MKCOL": true,
110 | "COPY": true,
111 | "MOVE": true,
112 | "LOCK": true,
113 | "UNLOCK": true,
114 | }
115 | )
116 |
117 | func IsPullMethod(method string) bool {
118 | _, ok := PullMethods[method]
119 | return ok
120 | }
121 |
122 | func IsPushMethod(method string) bool {
123 | _, ok := PushMethods[method]
124 | return ok
125 | }
126 |
127 | func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
128 | //log.Println("DAV:", r.RemoteAddr, r.Method, r.URL)
129 |
130 | switch r.Method {
131 | case "OPTIONS":
132 | s.doOptions(w, r)
133 |
134 | case "GET":
135 | s.doGet(w, r)
136 | case "HEAD":
137 | s.doHead(w, r)
138 | case "POST":
139 | s.doPost(w, r)
140 | case "DELETE":
141 | s.doDelete(w, r)
142 | case "PUT":
143 | s.doPut(w, r)
144 |
145 | case "PROPFIND":
146 | s.doPropfind(w, r)
147 | case "PROPPATCH":
148 | s.doProppatch(w, r)
149 | case "MKCOL":
150 | s.doMkcol(w, r)
151 | case "COPY":
152 | s.doCopy(w, r)
153 | case "MOVE":
154 | s.doMove(w, r)
155 |
156 | case "LOCK":
157 | s.doLock(w, r)
158 | case "UNLOCK":
159 | s.doUnlock(w, r)
160 |
161 | default:
162 | qlog.Error("DAV:", "unknown method", r.Method)
163 | w.WriteHeader(StatusBadRequest)
164 | }
165 | }
166 |
167 | func (s *Server) methodsAllowed(path string) string {
168 | if !s.pathExists(path) {
169 | return "OPTIONS, MKCOL, PUT, LOCK"
170 | }
171 |
172 | allowed := "OPTIONS, GET, HEAD, POST, DELETE, TRACE, PROPPATCH, COPY, MOVE, LOCK, UNLOCK"
173 |
174 | if s.Listings {
175 | allowed += ", PROPFIND"
176 | }
177 |
178 | if s.pathIsDirectory(path) {
179 | allowed += ", PUT"
180 | }
181 |
182 | return allowed
183 | }
184 |
185 | // convert request url to path
186 | func (s *Server) url2path(u *url.URL) string {
187 | if u.Path == "" {
188 | return "/"
189 | }
190 |
191 | if p := strings.TrimPrefix(u.Path, s.TrimPrefix); len(p) < len(u.Path) {
192 | return strings.Trim(p, "/")
193 | }
194 |
195 | return "/"
196 | }
197 |
198 | // convert path to url
199 | func (s *Server) path2url(p string) *url.URL {
200 | return &url.URL{Path: path.Join("/", s.TrimPrefix, p)}
201 | }
202 |
203 | // does path exists?
204 | func (s *Server) pathExists(path string) bool {
205 | f, err := s.Fs.Open(path)
206 | if err != nil {
207 | return false
208 | }
209 | defer f.Close()
210 |
211 | return true
212 | }
213 |
214 | // is path a directory?
215 | func (s *Server) pathIsDirectory(path string) bool {
216 | f, err := s.Fs.Open(path)
217 | if err != nil {
218 | return false
219 | }
220 | defer f.Close()
221 |
222 | fi, err := f.Stat()
223 | if err != nil {
224 | return false
225 | }
226 |
227 | return fi.IsDir()
228 | }
229 |
230 | func (s *Server) directoryContents(path string) []string {
231 | f, err := s.Fs.Open(path)
232 | if err != nil {
233 | return nil
234 | }
235 | defer f.Close()
236 |
237 | fi, err := f.Readdir(0)
238 | if err != nil {
239 | return nil
240 | }
241 |
242 | ret := make([]string, len(fi))
243 | for k, i := range fi {
244 | name := i.Name()
245 | if i.IsDir() {
246 | name += "/"
247 | }
248 | ret[k] = name
249 | }
250 |
251 | return ret
252 | }
253 |
254 | // is path in request locked?
255 | func (s *Server) isLockedRequest(r *http.Request) bool {
256 | return s.isLocked(
257 | s.url2path(r.URL),
258 | r.Header.Get("If") /*+r.Header.Get("Lock-Token")*/)
259 | }
260 |
261 | // is path locked?
262 | func (s *Server) isLocked(path, ifHeader string) bool {
263 | token, ok := s.path_to_token[path]
264 | if !ok {
265 | return false
266 | }
267 |
268 | if ifHeader == "" {
269 | return true
270 | }
271 |
272 | taglist := IfParser(ifHeader)
273 | found := false
274 | for _, tag := range taglist {
275 | for _, listitem := range tag.list {
276 | token = tokenFinder(listitem)
277 | if (token != "") &&
278 | s.hasLock(token) &&
279 | (s.getLock(token).uri == path) {
280 | found = true
281 | break
282 | }
283 | }
284 | if found {
285 | break
286 | }
287 | }
288 |
289 | return !found
290 | }
291 |
292 | func (s *Server) hasLock(token string) bool {
293 | _, ok := s.tokens_to_lock[token]
294 | return ok
295 | }
296 |
297 | func (s *Server) getToken(uri string) string {
298 | return s.path_to_token[uri]
299 | }
300 |
301 | func (s *Server) getLock(token string) *Lock {
302 | return s.tokens_to_lock[token]
303 | }
304 |
305 | func (s *Server) delLock(token string) {
306 | if lock, ok := s.tokens_to_lock[token]; ok {
307 | delete(s.path_to_token, lock.uri)
308 | delete(s.tokens_to_lock, token)
309 | }
310 | }
311 |
312 | func (s *Server) setLock(lock *Lock) {
313 | s.tokens_to_lock[lock.token] = lock
314 | s.path_to_token[lock.uri] = lock.token
315 | }
316 |
317 | func (s *Server) lockResource(path string) {
318 | // TODO
319 | }
320 |
321 | func (s *Server) unlockResource(path string) {
322 | // TODO
323 | }
324 |
325 | // The PROPFIND method retrieves properties defined on the resource identified by the Request-URI
326 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
327 | func (s *Server) doPropfind(w http.ResponseWriter, r *http.Request) {
328 | if !s.Listings {
329 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL)))
330 | w.WriteHeader(StatusMethodNotAllowed)
331 | return
332 | }
333 |
334 | depth := r.Header.Get("Depth")
335 | switch depth {
336 | case "0", "1":
337 | case "", "infinity":
338 | // treat as infinity if no depth header was included
339 | // disable infinity for performance and security concerns
340 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.1.1
341 | w.WriteHeader(StatusForbidden)
342 | return
343 | default:
344 | w.WriteHeader(StatusBadRequest)
345 | return
346 | }
347 |
348 | var propnames bool
349 | var properties []string
350 | var includes []string
351 |
352 | if r.ContentLength > 0 {
353 | propfind, err := NodeFromXml(r.Body)
354 | if err != nil {
355 | w.WriteHeader(StatusBadRequest)
356 | return
357 | }
358 |
359 | if propfind.Name.Local != "propfind" {
360 | w.WriteHeader(StatusBadRequest)
361 | return
362 | }
363 |
364 | // find by property
365 | // http://www.webdav.org/specs/rfc4918.html#dav.properties
366 | if propfind.HasChildren("prop") {
367 | prop := propfind.FirstChildren("prop")
368 | for _, p := range prop.GetChildrens("*") {
369 | properties = append(properties, p.Name.Local)
370 | }
371 | }
372 |
373 | // find property names
374 | if propfind.HasChildren("propname") {
375 | propnames = true
376 | }
377 |
378 | // find all properties
379 | if propfind.HasChildren("allprop") {
380 | properties = []string{
381 | "creationdate", "displayname",
382 | "getcontentlanguage", "getcontentlength",
383 | "getcontenttype", "getetag",
384 | "getlastmodified", "lockdiscovery",
385 | "resourcetype", "supportedlock",
386 | }
387 |
388 | if propfind.HasChildren("include") {
389 | for _, i := range propfind.GetChildrens("include") {
390 | includes = append(includes, i.Name.Local)
391 | }
392 | }
393 | }
394 | }
395 |
396 | path := s.url2path(r.URL)
397 | if !s.pathExists(path) {
398 | http.Error(w, path, StatusNotFound)
399 | // TODO: if locked (parent locked?) return multistatus with locked error as propstat
400 | return
401 | }
402 |
403 | paths := []string{path}
404 | if depth == "1" {
405 | // fetch all files if directory
406 | // TODO: respect []includes
407 |
408 | if s.pathIsDirectory(path) {
409 | for _, p := range s.directoryContents(path) {
410 | paths = append(paths, path+"/"+p)
411 | }
412 | }
413 | }
414 |
415 | buf := new(bytes.Buffer)
416 | buf.WriteString(``)
417 | buf.WriteString(``)
418 |
419 | // TODO: https?
420 | abs := "http://" + r.Host + s.TrimPrefix
421 |
422 | for _, p := range paths {
423 | // TODO
424 | // test locks/ authorization
425 | // if properties, show only given properties, else all
426 | // if propnames, return names of properties, else names and values
427 |
428 | propertiesNotFound := []string{}
429 |
430 | f, _ := s.Fs.Open(p)
431 | defer f.Close()
432 | fi, _ := f.Stat()
433 |
434 | buf.WriteString(``)
435 | buf.WriteString(`` + abs + "/" + p + ``)
436 | buf.WriteString(``)
437 | {
438 | buf.WriteString(``)
439 | {
440 | // TODO: make less ugly
441 | for _, prop := range properties {
442 |
443 | switch prop {
444 | case "creationdate":
445 | if propnames {
446 | buf.WriteString(`<` + prop + `/>`)
447 | } else {
448 | buf.WriteString(`<` + prop + `>`)
449 | buf.WriteString(fi.ModTime().Format("2006-01-02T15:04:05Z07:00"))
450 | buf.WriteString(`` + prop + `>`)
451 | }
452 | case "getcontentlanguage":
453 | if propnames {
454 | buf.WriteString(`<` + prop + `/>`)
455 | } else {
456 | buf.WriteString(`<` + prop + `>`)
457 | buf.WriteString(`en`)
458 | buf.WriteString(`` + prop + `>`)
459 | }
460 | case "getcontentlength":
461 | if fi.IsDir() {
462 | } else if propnames {
463 | buf.WriteString(`<` + prop + `/>`)
464 | } else {
465 | buf.WriteString(`<` + prop + `>`)
466 | buf.WriteString(strconv.FormatInt(int64(fi.Size()), 10))
467 | buf.WriteString(`` + prop + `>`)
468 | }
469 | case "getcontenttype":
470 | if fi.IsDir() {
471 | } else if propnames {
472 | buf.WriteString(`<` + prop + `/>`)
473 | } else {
474 | buf.WriteString(`<` + prop + `>`)
475 | buf.WriteString(mime.TypeByExtension(filepath.Ext(fi.Name())))
476 | buf.WriteString(`` + prop + `>`)
477 | }
478 | case "getlastmodified":
479 | if fi.IsDir() {
480 | } else if propnames {
481 | buf.WriteString(`<` + prop + `/>`)
482 | } else {
483 | buf.WriteString(`<` + prop + `>`)
484 | buf.WriteString(fi.ModTime().Format("Mon, 02 Jan 2006 15:04:05 MST"))
485 | buf.WriteString(`` + prop + `>`)
486 | }
487 | case "resourcetype":
488 | if propnames || !fi.IsDir() {
489 | // ZODO: reson for all the ugliness
490 | buf.WriteString(`<` + prop + `/>`)
491 | } else {
492 | buf.WriteString(`<` + prop + `>`)
493 | buf.WriteString(``)
494 | buf.WriteString(`` + prop + `>`)
495 | }
496 | case "displayname":
497 | if propnames {
498 | buf.WriteString(`<` + prop + `/>`)
499 | } else {
500 | buf.WriteString(`<` + prop + `>`)
501 | buf.WriteString(fi.Name())
502 | buf.WriteString(`` + prop + `>`)
503 | }
504 | case "supportedlock":
505 | if propnames {
506 | buf.WriteString(`<` + prop + `/>`)
507 | } else {
508 | buf.WriteString(`<` + prop + `>`)
509 | buf.WriteString(``)
510 | buf.WriteString(``)
511 | buf.WriteString(`` + prop + `>`)
512 | }
513 |
514 | // TODO: implement later at locks-stage
515 | // case "getetag": // not for dir
516 | // case "lockdiscovery":
517 | default:
518 | propertiesNotFound = append(propertiesNotFound, prop)
519 | }
520 | }
521 | }
522 | buf.WriteString(``)
523 | buf.WriteString(`HTTP/1.1 200 OK`)
524 | }
525 | buf.WriteString(``)
526 |
527 | if len(propertiesNotFound) > 0 {
528 | buf.WriteString(``)
529 | {
530 | buf.WriteString(``)
531 | {
532 | for _, prop := range propertiesNotFound {
533 | buf.WriteString(`<` + prop + `/>`)
534 | }
535 | }
536 | buf.WriteString(``)
537 | buf.WriteString(`HTTP/1.1 404 ` + StatusText(404) + ``)
538 | }
539 | buf.WriteString(``)
540 | }
541 |
542 | buf.WriteString(``)
543 | }
544 |
545 | buf.WriteString(``)
546 |
547 | w.WriteHeader(StatusMulti)
548 | w.Header().Set("Content-Length", string(buf.Len()))
549 | w.Header().Set("Content-Type", "application/xml; charset=utf-8")
550 |
551 | buf.WriteTo(w)
552 | // TODO: possible write error is suppressed
553 | }
554 |
555 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPPATCH
556 | func (s *Server) doProppatch(w http.ResponseWriter, r *http.Request) {
557 | if s.ReadOnly {
558 | w.WriteHeader(StatusForbidden)
559 | return
560 | }
561 |
562 | if s.isLockedRequest(r) {
563 | w.WriteHeader(StatusLocked)
564 | return
565 | }
566 |
567 | // TODO: proppatch
568 | w.WriteHeader(StatusNotImplemented)
569 | }
570 |
571 | // http://www.webdav.org/specs/rfc4918.html#METHOD_MKCOL
572 | func (s *Server) doMkcol(w http.ResponseWriter, r *http.Request) {
573 | if s.ReadOnly {
574 | w.WriteHeader(StatusForbidden)
575 | return
576 | }
577 |
578 | if s.isLockedRequest(r) {
579 | w.WriteHeader(StatusLocked)
580 | return
581 | }
582 |
583 | path := s.url2path(r.URL)
584 | if s.pathExists(path) {
585 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL)))
586 | w.WriteHeader(StatusMethodNotAllowed)
587 | return
588 | }
589 |
590 | // MKCOL may contain messagebody, precise behavior is undefined
591 | if r.ContentLength > 0 {
592 | _, err := NodeFromXml(r.Body)
593 | if err != nil {
594 | w.WriteHeader(StatusBadRequest)
595 | return
596 | }
597 |
598 | w.WriteHeader(StatusUnsupportedMediaType)
599 | return
600 | }
601 |
602 | if err := s.Fs.Mkdir(path); err != nil {
603 | w.WriteHeader(StatusConflict)
604 | return
605 | }
606 |
607 | w.WriteHeader(StatusCreated)
608 | s.unlockResource(path)
609 | }
610 |
611 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.4
612 | func (s *Server) doGet(w http.ResponseWriter, r *http.Request) {
613 | s.serveResource(w, r, true)
614 | }
615 |
616 | // http://www.webdav.org/specs/rfc4918.html#rfc.section.9.4
617 | func (s *Server) doHead(w http.ResponseWriter, r *http.Request) {
618 | s.serveResource(w, r, false)
619 | }
620 |
621 | // http://www.webdav.org/specs/rfc4918.html#METHOD_POST
622 | func (s *Server) doPost(w http.ResponseWriter, r *http.Request) {
623 | s.doGet(w, r)
624 | }
625 |
626 | func (s *Server) serveResource(w http.ResponseWriter, r *http.Request, serveContent bool) {
627 | path := s.url2path(r.URL)
628 |
629 | f, err := s.Fs.Open(path)
630 | if err != nil {
631 | http.Error(w, r.RequestURI, StatusNotFound)
632 | return
633 | }
634 | defer f.Close()
635 |
636 | // TODO: what if path is collection?
637 |
638 | fi, err := f.Stat()
639 | if err != nil {
640 | http.Error(w, r.RequestURI, StatusNotFound)
641 | return
642 | }
643 | modTime := fi.ModTime()
644 |
645 | if serveContent {
646 | http.ServeContent(w, r, path, modTime, f)
647 | } else {
648 | // TODO: better way to send only head
649 | http.ServeContent(w, r, path, modTime, emptyFile{})
650 | }
651 | }
652 |
653 | // http://www.webdav.org/specs/rfc4918.html#METHOD_DELETE
654 | func (s *Server) doDelete(w http.ResponseWriter, r *http.Request) {
655 | if s.ReadOnly {
656 | w.WriteHeader(StatusForbidden)
657 | return
658 | }
659 |
660 | if s.isLockedRequest(r) {
661 | w.WriteHeader(StatusLocked)
662 | return
663 | }
664 |
665 | s.deleteResource(s.url2path(r.URL), w, r, true)
666 | }
667 |
668 | func (s *Server) deleteResource(path string, w http.ResponseWriter, r *http.Request, setStatus bool) bool {
669 | ifHeader := r.Header.Get("If")
670 | lockToken := r.Header.Get("Lock-Token")
671 |
672 | if s.isLocked(path, ifHeader+lockToken) {
673 | w.WriteHeader(StatusLocked)
674 | return false
675 | }
676 |
677 | if !s.pathExists(path) {
678 | w.WriteHeader(StatusNotFound)
679 | return false
680 | }
681 |
682 | if !s.pathIsDirectory(path) {
683 | if err := s.Fs.Remove(path); err != nil {
684 | w.WriteHeader(StatusInternalServerError)
685 | return false
686 | }
687 | } else {
688 | // http://www.webdav.org/specs/rfc4918.html#delete-collections
689 | errors := map[string]int{}
690 | s.deleteCollection(path, w, r, errors)
691 |
692 | if err := s.Fs.Remove(path); err != nil {
693 | errors[path] = StatusInternalServerError
694 | }
695 |
696 | if len(errors) != 0 {
697 | // send multistatus
698 | abs := r.RequestURI
699 |
700 | buf := new(bytes.Buffer)
701 | buf.WriteString(``)
702 | buf.WriteString(``)
703 |
704 | for p, e := range errors {
705 | buf.WriteString(``)
706 | buf.WriteString(`` + abs + p + ``)
707 | buf.WriteString(`HTTP/1.1 ` + string(e) + ` ` + StatusText(e) + ``)
708 | buf.WriteString(``)
709 | }
710 |
711 | buf.WriteString(``)
712 |
713 | w.WriteHeader(StatusMulti)
714 | w.Header().Set("Content-Length", string(buf.Len()))
715 | w.Header().Set("Content-Type", "application/xml; charset=utf-8")
716 | buf.WriteTo(w)
717 |
718 | return false
719 | }
720 | }
721 |
722 | if setStatus {
723 | w.WriteHeader(StatusNoContent)
724 | }
725 | return true
726 | }
727 |
728 | func (s *Server) deleteCollection(path string, w http.ResponseWriter, r *http.Request, errors map[string]int) {
729 | ifHeader := r.Header.Get("If")
730 | lockToken := r.Header.Get("Lock-Token")
731 |
732 | for _, p := range s.directoryContents(path) {
733 | p = path + "/" + p
734 |
735 | if s.isLocked(p, ifHeader+lockToken) {
736 | errors[p] = StatusLocked
737 | } else {
738 | if s.pathIsDirectory(p) {
739 | s.deleteCollection(p, w, r, errors)
740 | }
741 |
742 | if err := s.Fs.Remove(p); err != nil {
743 | errors[p] = StatusInternalServerError
744 | }
745 | }
746 | }
747 |
748 | }
749 |
750 | // http://www.webdav.org/specs/rfc4918.html#METHOD_PUT
751 | func (s *Server) doPut(w http.ResponseWriter, r *http.Request) {
752 | if s.ReadOnly {
753 | w.WriteHeader(StatusForbidden)
754 | return
755 | }
756 |
757 | if s.isLockedRequest(r) {
758 | w.WriteHeader(StatusLocked)
759 | return
760 | }
761 |
762 | path := s.url2path(r.URL)
763 |
764 | if s.pathIsDirectory(path) {
765 | // use MKCOL instead
766 | w.WriteHeader(StatusMethodNotAllowed)
767 | return
768 | }
769 |
770 | exists := s.pathExists(path)
771 |
772 | // TODO: content range / partial put
773 |
774 | // truncate file if exists
775 | file, err := s.Fs.Create(path)
776 | if err != nil {
777 | w.WriteHeader(StatusConflict)
778 | return
779 | }
780 | defer file.Close()
781 |
782 | if _, err := io.Copy(file, r.Body); err != nil {
783 | w.WriteHeader(StatusConflict)
784 | } else {
785 | if exists {
786 | w.WriteHeader(StatusNoContent)
787 | } else {
788 | w.WriteHeader(StatusCreated)
789 | }
790 | }
791 |
792 | s.unlockResource(path)
793 | }
794 |
795 | // http://www.webdav.org/specs/rfc4918.html#METHOD_COPY
796 | func (s *Server) doCopy(w http.ResponseWriter, r *http.Request) {
797 | if s.ReadOnly {
798 | w.WriteHeader(StatusForbidden)
799 | return
800 | }
801 |
802 | s.copyResource(w, r)
803 | }
804 |
805 | // http://www.webdav.org/specs/rfc4918.html#METHOD_MOVE
806 | func (s *Server) doMove(w http.ResponseWriter, r *http.Request) {
807 | if s.ReadOnly {
808 | w.WriteHeader(StatusForbidden)
809 | return
810 | }
811 |
812 | if s.isLockedRequest(r) {
813 | w.WriteHeader(StatusLocked)
814 | return
815 | }
816 |
817 | if s.copyResource(w, r) {
818 | // TODO: duplicate http-header sent?
819 | s.deleteResource(s.url2path(r.URL), w, r, false)
820 | }
821 | }
822 |
823 | func (s *Server) copyResource(w http.ResponseWriter, r *http.Request) bool {
824 | dest := r.Header.Get("Destination")
825 | if dest == "" {
826 | w.WriteHeader(StatusBadRequest)
827 | return false
828 | }
829 |
830 | d, err := url.Parse(dest)
831 | if err != nil {
832 | w.WriteHeader(StatusBadRequest)
833 | return false
834 | }
835 | // TODO: normalize dest?
836 | dest = s.url2path(d)
837 | source := s.url2path(r.URL)
838 |
839 | // source equals destination
840 | if source == dest {
841 | w.WriteHeader(StatusForbidden)
842 | return false
843 | }
844 |
845 | // destination must be same server/namespace as source
846 | if d.Host != r.Host ||
847 | !strings.HasPrefix(d.Path, s.TrimPrefix) ||
848 | !strings.HasPrefix(r.URL.Path, s.TrimPrefix) {
849 |
850 | w.WriteHeader(StatusBadGateway)
851 | return false
852 | }
853 |
854 | // TODO: needs to be tested? should be catched with error at CopyFile returning StatusConflict
855 | // currently only at depth=0 or non-collection copy
856 | /*
857 | parentDest := dest[:strings.LastIndex(dest, "/")]
858 | if !s.pathExists(parentDest) {
859 | w.WriteHeader(StatusConflict)
860 | return false
861 | }
862 | */
863 |
864 | overwrite := r.Header.Get("Overwrite") != "F"
865 | exists := s.pathExists(dest)
866 |
867 | if overwrite {
868 | if exists {
869 | if !s.deleteResource(dest, w, r, false) {
870 | w.WriteHeader(StatusInternalServerError)
871 | return false
872 | }
873 | }
874 | } else {
875 | if exists {
876 | w.WriteHeader(StatusPreconditionFailed)
877 | return false
878 | }
879 | }
880 |
881 | if !s.pathIsDirectory(source) {
882 | if err := s.CopyFile(source, dest); err != nil {
883 | // TODO: always conflict? e.g. copy to non-existant path
884 | //w.WriteHeader(StatusInternalServerError)
885 | w.WriteHeader(StatusConflict)
886 | return false
887 | }
888 | } else if r.Header.Get("Depth") == "0" {
889 | // copy only collection, not its internal members
890 | // http://www.webdav.org/specs/rfc4918.html#copy.for.collections
891 | if err := s.Fs.Mkdir(dest); err != nil {
892 | w.WriteHeader(StatusConflict)
893 | return false
894 | }
895 | } else {
896 | // http://www.webdav.org/specs/rfc4918.html#copy.for.collections
897 | errors := map[string]int{}
898 |
899 | if err := s.Fs.Mkdir(dest); err != nil {
900 | errors[source] = StatusInternalServerError
901 | }
902 |
903 | s.copyCollection(source, dest, w, r, errors)
904 |
905 | if len(errors) != 0 {
906 | // send multistatus
907 | abs := r.RequestURI
908 |
909 | buf := new(bytes.Buffer)
910 | buf.WriteString(``)
911 | buf.WriteString(``)
912 |
913 | for p, e := range errors {
914 | buf.WriteString(``)
915 | buf.WriteString(`` + abs + p + ``)
916 | buf.WriteString(`HTTP/1.1 ` + string(e) + ` ` + StatusText(e) + ``)
917 | buf.WriteString(``)
918 | }
919 |
920 | buf.WriteString(``)
921 |
922 | w.WriteHeader(StatusMulti)
923 | w.Header().Set("Content-Length", string(buf.Len()))
924 | w.Header().Set("Content-Type", "application/xml; charset=utf-8")
925 | buf.WriteTo(w)
926 |
927 | return false
928 | }
929 | }
930 |
931 | // copy was successful
932 | if exists {
933 | w.WriteHeader(StatusNoContent)
934 | } else {
935 | w.WriteHeader(StatusCreated)
936 | }
937 |
938 | s.unlockResource(dest)
939 | return true
940 | }
941 |
942 | func (s *Server) CopyFile(source, dest string) error {
943 | // open source file
944 | fs, err := s.Fs.Open(source)
945 | if err != nil {
946 | return err
947 | }
948 | defer fs.Close()
949 |
950 | // open destination file
951 | fd, err := s.Fs.Create(dest)
952 | if err != nil {
953 | return err
954 | }
955 | defer fd.Close()
956 |
957 | // copy file contents
958 | if _, err := io.Copy(fd, fs); err != nil {
959 | return err
960 | }
961 |
962 | // TODO: copy file stats? http://www.webdav.org/specs/rfc4918.html#copy.for.properties
963 |
964 | return nil
965 | }
966 |
967 | func (s *Server) copyCollection(source, dest string, w http.ResponseWriter, r *http.Request, errors map[string]int) {
968 | ifHeader := r.Header.Get("If")
969 | lockToken := r.Header.Get("Lock-Token")
970 |
971 | for _, sub := range s.directoryContents(source) {
972 | ssub := source + "/" + sub
973 | dsub := dest + "/" + sub
974 |
975 | if s.isLocked(ssub, ifHeader+lockToken) {
976 | errors[ssub] = StatusLocked
977 | } else {
978 | if s.pathIsDirectory(ssub) {
979 | if err := s.Fs.Mkdir(dsub); err != nil {
980 | errors[ssub] = StatusInternalServerError
981 | }
982 |
983 | s.copyCollection(ssub, dsub, w, r, errors)
984 | } else {
985 | if err := s.CopyFile(ssub, dsub); err != nil {
986 | errors[ssub] = StatusInternalServerError
987 | }
988 | }
989 | }
990 | }
991 | }
992 |
993 | /*func (s *Server) _lock_unlock_parse(body string) (map[string]string, error) {
994 | node, err := NodeFromXmlString(body)
995 | if err != nil {
996 | return nil, err
997 | }
998 |
999 | data := make(map[string]string)
1000 | if node != nil {
1001 | if node.Name.Local != "lockinfo" {
1002 | node = node.FirstChildren("lockinfo")
1003 | }
1004 | if node == nil {
1005 | return nil, errors.New("not lockinfo element")
1006 | }
1007 |
1008 | data["lockscope"] = node.FirstChildren("lockscope").Children[0].Name.Local
1009 |
1010 | data["locktype"] = node.FirstChildren("locktype").Children[0].Name.Local
1011 |
1012 | data["lockowner"] = node.FirstChildren("owner").Children[0].Value[7:]
1013 | }
1014 | return data, nil
1015 | }*/
1016 |
1017 | func (s *Server) _lock_unlock_create(lock *Lock, depth string) (string, string) {
1018 | //lock := &Lock{uri: uri, creator: creator}
1019 | iscollection := (lock.uri[len(lock.uri)-1] == '/') //# very dumb collection check
1020 |
1021 | result := ""
1022 | if depth == "infinity" && iscollection {
1023 | //# locking of children/collections not yet supported
1024 | //pass
1025 | }
1026 |
1027 | if !s.isLocked(lock.uri, "") {
1028 | s.setLock(lock)
1029 | }
1030 |
1031 | //# because we do not handle children we leave result empty
1032 | return lock.token, result
1033 | }
1034 |
1035 | /*
1036 | LOCK /workspace/webdav/proposal.doc HTTP/1.1
1037 | Host: example.com
1038 | Timeout: Infinite, Second-4100000000
1039 | Content-Type: application/xml; charset="utf-8"
1040 | Content-Length: xxxx
1041 | Authorization: Digest username="ejw",
1042 | realm="ejw@example.com", nonce="...",
1043 | uri="/workspace/webdav/proposal.doc",
1044 | response="...", opaque="..."
1045 |
1046 |
1047 |
1048 |
1049 |
1050 |
1051 | http://example.org/~ejw/contact.html
1052 |
1053 |
1054 | */
1055 |
1056 | func (s *Server) doLock(w http.ResponseWriter, r *http.Request) {
1057 | if s.ReadOnly {
1058 | w.WriteHeader(StatusForbidden)
1059 | return
1060 | }
1061 |
1062 | if s.isLockedRequest(r) {
1063 | w.WriteHeader(StatusLocked)
1064 | return
1065 | }
1066 |
1067 | //dc = self.IFACE_CLASS
1068 |
1069 | qlog.Info("LOCKing resource %s", r.Header)
1070 |
1071 | bbody, err := ioutil.ReadAll(r.Body)
1072 | if err != nil {
1073 | fmt.Println(err)
1074 | w.WriteHeader(500)
1075 | return
1076 | }
1077 | var body = string(bbody)
1078 |
1079 | depth := "infinity"
1080 | if r.Header.Get("Depth") != "" {
1081 | depth = r.Header.Get("Depth")
1082 | }
1083 |
1084 | //uri = urlparse.urljoin(self.get_baseuri(dc), self.path)
1085 | //uri = urllib.unquote(uri)
1086 | uri := r.RequestURI
1087 | qlog.Info("do_LOCK: uri = %s", uri)
1088 |
1089 | ifheader := r.Header.Get("If")
1090 | alreadylocked := s.isLocked(uri, ifheader)
1091 | qlog.Info("do_LOCK: alreadylocked = %s", alreadylocked)
1092 |
1093 | if body != "" && alreadylocked {
1094 | //# Full LOCK request but resource already locked
1095 | //self.responses[423] = ('Locked', 'Already locked')
1096 | w.WriteHeader(423)
1097 | return
1098 | } else if body != "" && ifheader == "" {
1099 | //# LOCK with XML information
1100 | //fmt.Println("body:", body)
1101 | lock, err := ParseLockString(body)
1102 | //data, err := s._lock_unlock_parse(body)
1103 | if err != nil {
1104 | fmt.Println(err)
1105 | w.WriteHeader(500)
1106 | return
1107 | }
1108 | lock.timeout = ParseTimeOut(r)
1109 | lock.uri = r.RequestURI
1110 | lock.token = generateToken()
1111 | //fmt.Println("lock:", data)
1112 | token, result := s._lock_unlock_create(lock, depth)
1113 |
1114 | if result != "" {
1115 | w.Write([]byte(result))
1116 | w.Header().Set("Content-Type", "text/xml; charset=utf-8")
1117 | w.WriteHeader(207)
1118 | } else {
1119 | //lock := s.getLock(token)
1120 | w.Header().Set("Lock-Token", fmt.Sprintf("", token))
1121 | w.Header().Set("Content-Type", "text/xml; charset=utf-8")
1122 | output := lock.asXML("D:", false)
1123 | fmt.Println("output:", output)
1124 | w.Write([]byte(output))
1125 | //w.WriteHeader(200)
1126 | }
1127 | } else {
1128 | //d# refresh request - refresh lock timeout
1129 | taglist := IfParser(ifheader)
1130 | var found bool
1131 | for _, tag := range taglist {
1132 | for _, listitem := range tag.list {
1133 | token := tokenFinder(listitem)
1134 | if token != "" && s.hasLock(token) {
1135 | lock := s.getLock(token)
1136 | timeout := "Infinite"
1137 | if r.Header.Get("Timeout") != "" {
1138 | timeout = r.Header.Get("Timeout")
1139 | }
1140 | to, _ := strconv.Atoi(timeout)
1141 | lock.SetTimeout(time.Duration(to)) //# automatically refreshes
1142 | found = true
1143 |
1144 | w.WriteHeader(200)
1145 | w.Write([]byte(lock.asXML("", true)))
1146 | w.Header().Set("Content-Type", "text/xml; encoding=utf-8")
1147 | break
1148 | }
1149 | }
1150 | if found {
1151 | break
1152 | }
1153 | }
1154 | //# we didn't find any of the tokens mentioned - means
1155 | //# that table was cleared or another error
1156 | if !found {
1157 | w.WriteHeader(412) //a# precondition failed
1158 | }
1159 | }
1160 | }
1161 |
1162 | // takes a string like ' and returns the token
1163 | // part.
1164 | func tokenFinder(token string) string {
1165 | if token == "" {
1166 | return ""
1167 | }
1168 | if token[0] == '[' {
1169 | return ""
1170 | }
1171 | if token[0] == '<' {
1172 | token = token[1 : len(token)-1]
1173 | }
1174 | return token[strings.Index(token, ":")+1:]
1175 | }
1176 |
1177 | func (s *Server) doUnlock(w http.ResponseWriter, r *http.Request) {
1178 | /*if s.ReadOnly {
1179 | w.WriteHeader(StatusForbidden)
1180 | return
1181 | }
1182 |
1183 | if s.isLockedRequest(r) {
1184 | w.WriteHeader(StatusLocked)
1185 | return
1186 | }
1187 |
1188 | // TODO: unlock
1189 | w.WriteHeader(StatusNotImplemented)
1190 | return*/
1191 |
1192 | //dc = self.IFACE_CLASS
1193 |
1194 | //if self._config.DAV.getboolean('verbose') is True:
1195 | qlog.Info("UNLOCKing resource", r.Header)
1196 |
1197 | //uri := urlparse.urljoin(self.get_baseuri(dc), self.path)
1198 | //uri = urllib.unquote(uri)
1199 | uri := r.RequestURI
1200 |
1201 | // check lock token - must contain a dash
1202 | lockToken := r.Header.Get("Lock-Token")
1203 | if !strings.Contains(lockToken, "-") {
1204 | w.WriteHeader(400)
1205 | return
1206 | }
1207 |
1208 | ifHeader := r.Header.Get("If")
1209 | token := tokenFinder(lockToken)
1210 | if s.isLocked(uri, ifHeader) {
1211 | s.delLock(token)
1212 | }
1213 |
1214 | w.WriteHeader(204)
1215 | //self.send_body(None, '204', 'Ok', 'Ok')
1216 | }
1217 |
1218 | func (s *Server) doOptions(w http.ResponseWriter, r *http.Request) {
1219 | // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes
1220 | w.Header().Set("DAV", "1, 2")
1221 |
1222 | w.Header().Set("Allow", s.methodsAllowed(s.url2path(r.URL)))
1223 | w.Header().Set("MS-Author-Via", "DAV")
1224 | }
1225 |
--------------------------------------------------------------------------------
/util.go:
--------------------------------------------------------------------------------
1 | package webdav
2 |
3 | import (
4 | "fmt"
5 | "math/rand"
6 | "net/http"
7 | "regexp"
8 | "strconv"
9 | "strings"
10 | "time"
11 | )
12 |
13 | type Depth string
14 |
15 | var (
16 | NoDepth = Depth("")
17 | Depth0 = Depth("0")
18 | Depth1 = Depth("1")
19 | DepthInfinity = Depth("infinity")
20 | )
21 |
22 | func ParseDepth(r *http.Request) Depth {
23 | return Depth(r.Header.Get("Depth"))
24 | }
25 |
26 | type LockToken string
27 |
28 | var (
29 | NoLockToken = LockToken("")
30 | )
31 |
32 | func GenToken() LockToken {
33 | r := rand.New(rand.NewSource(time.Now().UnixNano()))
34 | return LockToken(fmt.Sprintf("%s-%s-00105A989226:%.03f",
35 | r.Int31(), r.Int31(), time.Now().UnixNano()))
36 | }
37 |
38 | func ParseToken(r *http.Request) LockToken {
39 | return ""
40 | }
41 |
42 | var IfHdr = regexp.MustCompile(`(?P<.+?>)?\s*\((?P[^)]+)\)`)
43 |
44 | var ListItem = regexp.MustCompile(
45 | `(?Pnot)?\s*(?P<[a-zA-Z]+:[^>]*>|\[.*?\])`)
46 |
47 | type TagList struct {
48 | resource string
49 | list []string
50 | NOTTED int
51 | }
52 |
53 | func IfParser(hdr string) []*TagList {
54 | out := make([]*TagList, 0)
55 | /*i := 0
56 | for {
57 | m := IfHdr.FindString(hdr[i:])
58 | if m == ""{
59 | break
60 | }
61 |
62 | i = i + m.end()
63 | tag := new(TagList)
64 | tag.resource = m.group("resource")
65 | // We need to delete < >
66 | if tag.resource != "" {
67 | tag.resource = tag.resource[1:-1]
68 | }
69 | listitem = m.group("listitem")
70 | tag.NOTTED, tag.list = ListParser(listitem)
71 | append(out, tag)
72 | }*/
73 |
74 | return out
75 | }
76 |
77 | const (
78 | infinite = "Infinite"
79 | )
80 |
81 | type TimeOut time.Duration
82 |
83 | func (to TimeOut) String() string {
84 | fmt.Println("===", time.Duration(to), "===")
85 | if int64(to) == 0 {
86 | return infinite
87 | }
88 | return fmt.Sprintf("Second-%d", time.Duration(to)/time.Second)
89 | }
90 |
91 | // Infinite, Second-4100000000
92 | func ParseTimeOut(req *http.Request) TimeOut {
93 | tm := req.Header.Get("Timeout")
94 | if tm == "" || tm == infinite {
95 | return TimeOut(0)
96 | }
97 |
98 | ss := strings.Split(tm, "-")
99 | if len(ss) != 2 {
100 | return TimeOut(0)
101 | }
102 |
103 | a, err := strconv.Atoi(ss[1])
104 | if err != nil {
105 | return TimeOut(0)
106 | }
107 | return TimeOut(time.Duration(a) * time.Second)
108 | }
109 |
110 | func IsOverwrite(req *http.Request) bool {
111 | ow := req.Header.Get("Overwrite")
112 | return ow == "T" || ow == ""
113 | }
114 |
--------------------------------------------------------------------------------