go.net/webdav: new Handler, FileSystem, LockSystem and lockInfo types.

LGTM=dave
R=nmvc, dave
CC=bradfitz, dr.volker.dobler, golang-codereviews
https://golang.org/cl/169240043
diff --git a/webdav/webdav.go b/webdav/webdav.go
new file mode 100644
index 0000000..7ffb859
--- /dev/null
+++ b/webdav/webdav.go
@@ -0,0 +1,295 @@
+// Copyright 2014 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// Package webdav etc etc TODO.
+package webdav
+
+// TODO: ETag, properties.
+// TODO: figure out what/when is responsible for path cleaning: no "../../etc/passwd"s.
+
+import (
+	"errors"
+	"io"
+	"net/http"
+	"os"
+	"time"
+)
+
+// TODO: define the PropSystem interface.
+type PropSystem interface{}
+
+type Handler struct {
+	// FileSystem is the virtual file system.
+	FileSystem FileSystem
+	// LockSystem is the lock management system.
+	LockSystem LockSystem
+	// PropSystem is an optional property management system. If non-nil, TODO.
+	PropSystem PropSystem
+	// Logger is an optional error logger. If non-nil, it will be called
+	// whenever handling a http.Request results in an error.
+	Logger func(*http.Request, error)
+}
+
+func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+	status, err := http.StatusBadRequest, error(nil)
+	if h.FileSystem == nil {
+		status, err = http.StatusInternalServerError, errNoFileSystem
+	} else if h.LockSystem == nil {
+		status, err = http.StatusInternalServerError, errNoLockSystem
+	} else {
+		// TODO: COPY, MOVE, PROPFIND, PROPPATCH methods. Also, OPTIONS??
+		switch r.Method {
+		case "GET", "HEAD", "POST":
+			status, err = h.handleGetHeadPost(w, r)
+		case "DELETE":
+			status, err = h.handleDelete(w, r)
+		case "PUT":
+			status, err = h.handlePut(w, r)
+		case "MKCOL":
+			status, err = h.handleMkcol(w, r)
+		case "LOCK":
+			status, err = h.handleLock(w, r)
+		case "UNLOCK":
+			status, err = h.handleUnlock(w, r)
+		}
+	}
+
+	if status != 0 {
+		w.WriteHeader(status)
+		if status != http.StatusNoContent {
+			w.Write([]byte(StatusText(status)))
+		}
+	}
+	if h.Logger != nil && err != nil {
+		h.Logger(r, err)
+	}
+}
+
+func (h *Handler) confirmLocks(r *http.Request) (closer io.Closer, status int, err error) {
+	ih, ok := parseIfHeader(r.Header.Get("If"))
+	if !ok {
+		return nil, http.StatusBadRequest, errInvalidIfHeader
+	}
+	// ih is a disjunction (OR) of ifLists, so any ifList will do.
+	for _, l := range ih.lists {
+		path := l.resourceTag
+		if path == "" {
+			path = r.URL.Path
+		}
+		closer, err = h.LockSystem.Confirm(path, l.conditions...)
+		if err == ErrConfirmationFailed {
+			continue
+		}
+		if err != nil {
+			return nil, http.StatusInternalServerError, err
+		}
+		return closer, 0, nil
+	}
+	return nil, http.StatusPreconditionFailed, errLocked
+}
+
+func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	// TODO: check locks for read-only access??
+	f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDONLY, 0)
+	if err != nil {
+		return http.StatusNotFound, err
+	}
+	defer f.Close()
+	fi, err := f.Stat()
+	if err != nil {
+		return http.StatusNotFound, err
+	}
+	http.ServeContent(w, r, r.URL.Path, fi.ModTime(), f)
+	return 0, nil
+}
+
+func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	closer, status, err := h.confirmLocks(r)
+	if err != nil {
+		return status, err
+	}
+	defer closer.Close()
+
+	if err := h.FileSystem.RemoveAll(r.URL.Path); err != nil {
+		// TODO: MultiStatus.
+		return http.StatusMethodNotAllowed, err
+	}
+	return http.StatusNoContent, nil
+}
+
+func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	closer, status, err := h.confirmLocks(r)
+	if err != nil {
+		return status, err
+	}
+	defer closer.Close()
+
+	f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+	if err != nil {
+		return http.StatusNotFound, err
+	}
+	defer f.Close()
+	if _, err := io.Copy(f, r.Body); err != nil {
+		return http.StatusMethodNotAllowed, err
+	}
+	return http.StatusCreated, nil
+}
+
+func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	closer, status, err := h.confirmLocks(r)
+	if err != nil {
+		return status, err
+	}
+	defer closer.Close()
+
+	if err := h.FileSystem.Mkdir(r.URL.Path, 0777); err != nil {
+		if os.IsNotExist(err) {
+			return http.StatusConflict, err
+		}
+		return http.StatusMethodNotAllowed, err
+	}
+	return http.StatusCreated, nil
+}
+
+func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus int, retErr error) {
+	duration, err := parseTimeout(r.Header.Get("Timeout"))
+	if err != nil {
+		return http.StatusBadRequest, err
+	}
+	li, status, err := readLockInfo(r.Body)
+	if err != nil {
+		return status, err
+	}
+
+	token, ld := "", LockDetails{}
+	if li == (lockInfo{}) {
+		// An empty lockInfo means to refresh the lock.
+		ih, ok := parseIfHeader(r.Header.Get("If"))
+		if !ok {
+			return http.StatusBadRequest, errInvalidIfHeader
+		}
+		if len(ih.lists) == 1 && len(ih.lists[0].conditions) == 1 {
+			token = ih.lists[0].conditions[0].Token
+		}
+		if token == "" {
+			return http.StatusBadRequest, errInvalidLockToken
+		}
+		var closer io.Closer
+		ld, closer, err = h.LockSystem.Refresh(token, time.Now(), duration)
+		if err != nil {
+			if err == ErrNoSuchLock {
+				return http.StatusPreconditionFailed, err
+			}
+			return http.StatusInternalServerError, err
+		}
+		defer closer.Close()
+
+	} else {
+		depth, err := parseDepth(r.Header.Get("Depth"))
+		if err != nil {
+			return http.StatusBadRequest, err
+		}
+		ld = LockDetails{
+			Depth:    depth,
+			Duration: duration,
+			OwnerXML: li.Owner.InnerXML,
+			Path:     r.URL.Path,
+		}
+		var closer io.Closer
+		token, closer, err = h.LockSystem.Create(r.URL.Path, time.Now(), ld)
+		if err != nil {
+			return http.StatusInternalServerError, err
+		}
+		defer func() {
+			if retErr != nil {
+				h.LockSystem.Unlock(token)
+			}
+		}()
+		defer closer.Close()
+
+		// Create the resource if it didn't previously exist.
+		if _, err := h.FileSystem.Stat(r.URL.Path); err != nil {
+			f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
+			if err != nil {
+				// TODO: detect missing intermediate dirs and return http.StatusConflict?
+				return http.StatusInternalServerError, err
+			}
+			f.Close()
+			w.WriteHeader(http.StatusCreated)
+			// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
+			// Lock-Token value is a Coded-URL. We add angle brackets.
+			w.Header().Set("Lock-Token", "<"+token+">")
+		}
+	}
+
+	w.Header().Set("Content-Type", "application/xml; charset=utf-8")
+	writeLockInfo(w, token, ld)
+	return 0, nil
+}
+
+func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status int, err error) {
+	// http://www.webdav.org/specs/rfc4918.html#HEADER_Lock-Token says that the
+	// Lock-Token value is a Coded-URL. We strip its angle brackets.
+	t := r.Header.Get("Lock-Token")
+	if len(t) < 2 || t[0] != '<' || t[len(t)-1] != '>' {
+		return http.StatusBadRequest, errInvalidLockToken
+	}
+	t = t[1 : len(t)-1]
+
+	switch err = h.LockSystem.Unlock(t); err {
+	case nil:
+		return http.StatusNoContent, err
+	case ErrForbidden:
+		return http.StatusForbidden, err
+	case ErrNoSuchLock:
+		return http.StatusConflict, err
+	default:
+		return http.StatusInternalServerError, err
+	}
+}
+
+func parseDepth(s string) (int, error) {
+	// TODO: implement.
+	return -1, nil
+}
+
+func parseTimeout(s string) (time.Duration, error) {
+	// TODO: implement.
+	return 1 * time.Second, nil
+}
+
+// http://www.webdav.org/specs/rfc4918.html#status.code.extensions.to.http11
+const (
+	StatusMulti               = 207
+	StatusUnprocessableEntity = 422
+	StatusLocked              = 423
+	StatusFailedDependency    = 424
+	StatusInsufficientStorage = 507
+)
+
+func StatusText(code int) string {
+	switch code {
+	case StatusMulti:
+		return "Multi-Status"
+	case StatusUnprocessableEntity:
+		return "Unprocessable Entity"
+	case StatusLocked:
+		return "Locked"
+	case StatusFailedDependency:
+		return "Failed Dependency"
+	case StatusInsufficientStorage:
+		return "Insufficient Storage"
+	}
+	return http.StatusText(code)
+}
+
+var (
+	errInvalidIfHeader     = errors.New("webdav: invalid If header")
+	errInvalidLockInfo     = errors.New("webdav: invalid lock info")
+	errInvalidLockToken    = errors.New("webdav: invalid lock token")
+	errLocked              = errors.New("webdav: locked")
+	errNoFileSystem        = errors.New("webdav: no file system")
+	errNoLockSystem        = errors.New("webdav: no lock system")
+	errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
+)