Commit 46077d3c authored by Nigel Tao's avatar Nigel Tao

webdav: first cut of a memLS implementation.

Change-Id: If5745dcc1d66fdaa083d4085440b1535aa75e0da
Reviewed-on: https://go-review.googlesource.com/2430Reviewed-by: 's avatarDave Cheney <dave@cheney.net>
parent 9818cd45
......@@ -6,13 +6,21 @@ package webdav
import (
"errors"
"io"
"path"
"strconv"
"strings"
"sync"
"time"
)
var (
// ErrConfirmationFailed is returned by a LockSystem's Confirm method.
ErrConfirmationFailed = errors.New("webdav: confirmation failed")
// ErrForbidden is returned by a LockSystem's Unlock method.
ErrForbidden = errors.New("webdav: forbidden")
// ErrLocked is returned by a LockSystem's Create, Refresh and Unlock methods.
ErrLocked = errors.New("webdav: locked")
// ErrNoSuchLock is returned by a LockSystem's Refresh and Unlock methods.
ErrNoSuchLock = errors.New("webdav: no such lock")
)
......@@ -24,21 +32,263 @@ type Condition struct {
ETag string
}
// Releaser releases previously confirmed lock claims.
//
// Calling Release does not unlock the lock, in the WebDAV UNLOCK sense, but
// once LockSystem.Confirm has confirmed that a lock claim is valid, that lock
// cannot be Confirmed again until it has been Released.
type Releaser interface {
Release()
}
// LockSystem manages access to a collection of named resources. The elements
// in a lock name are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention.
type LockSystem interface {
// TODO: comment that the conditions should be ANDed together.
Confirm(path string, conditions ...Condition) (c io.Closer, err error)
// TODO: comment that token should be an absolute URI as defined by RFC 3986,
// Section 4.3. In particular, it should not contain whitespace.
Create(path string, now time.Time, ld LockDetails) (token string, c io.Closer, err error)
Refresh(token string, now time.Time, duration time.Duration) (ld LockDetails, c io.Closer, err error)
Unlock(token string) error
// Confirm confirms that the caller can claim all of the locks specified by
// the given conditions, and that holding the union of all of those locks
// gives exclusive access to the named resource.
//
// Exactly one of r and err will be non-nil. If r is non-nil, all of the
// requested locks are held until r.Release is called.
//
// If Confirm returns ErrConfirmationFailed then the Handler will continue
// to try any other set of locks presented (a WebDAV HTTP request can
// present more than one set of locks). If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
Confirm(now time.Time, name string, conditions ...Condition) (r Releaser, err error)
// Create creates a lock with the given depth, duration, owner and root
// (name). The depth will either be negative (meaning infinite) or zero.
//
// If Create returns ErrLocked then the Handler will write a "423 Locked"
// HTTP status. If it returns any other non-nil error, the Handler will
// write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
//
// The token returned identifies the created lock. It should be an absolute
// URI as defined by RFC 3986, Section 4.3. In particular, it should not
// contain whitespace.
Create(now time.Time, details LockDetails) (token string, err error)
// Refresh refreshes the lock with the given token.
//
// If Refresh returns ErrLocked then the Handler will write a "423 Locked"
// HTTP Status. If Refresh returns ErrNoSuchLock then the Handler will write
// a "412 Precondition Failed" HTTP Status. If it returns any other non-nil
// error, the Handler will write a "500 Internal Server Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.10.6 for
// when to use each error.
Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error)
// Unlock unlocks the lock with the given token.
//
// If Unlock returns ErrForbidden then the Handler will write a "403
// Forbidden" HTTP Status. If Unlock returns ErrLocked then the Handler
// will write a "423 Locked" HTTP status. If Unlock returns ErrNoSuchLock
// then the Handler will write a "409 Conflict" HTTP Status. If it returns
// any other non-nil error, the Handler will write a "500 Internal Server
// Error" HTTP status.
//
// See http://www.webdav.org/specs/rfc4918.html#rfc.section.9.11.1 for
// when to use each error.
Unlock(now time.Time, token string) error
}
// LockDetails are a lock's metadata.
type LockDetails struct {
Depth int // Negative means infinite depth.
Duration time.Duration // Negative means unlimited duration.
OwnerXML string // Verbatim XML.
Path string
// Root is the root resource name being locked. For a zero-depth lock, the
// root is the only resource being locked.
Root string
// Depth is the lock depth. A negative depth means infinite.
//
// TODO: should depth be restricted to just "0 or infinite" (i.e. change
// this field to "Recursive bool") or just "0 or 1 or infinite"? Is
// validating that the responsibility of the Handler or the LockSystem
// implementations?
Depth int
// Duration is the lock timeout. A negative duration means infinite.
Duration time.Duration
// OwnerXML is the verbatim <owner> XML given in a LOCK HTTP request.
//
// TODO: does the "verbatim" nature play well with XML namespaces?
// Does the OwnerXML field need to have more structure? See
// https://codereview.appspot.com/175140043/#msg2
OwnerXML string
}
// NewMemLS returns a new in-memory LockSystem.
func NewMemLS() LockSystem {
return &memLS{
byName: make(map[string]*memLSNode),
byToken: make(map[string]*memLSNode),
gen: uint64(time.Now().Unix()),
}
}
type memLS struct {
mu sync.Mutex
byName map[string]*memLSNode
byToken map[string]*memLSNode
gen uint64
}
func (m *memLS) nextToken() string {
m.gen++
return strconv.FormatUint(m.gen, 10)
}
func (m *memLS) collectExpiredNodes(now time.Time) {
// TODO: implement.
}
func (m *memLS) Confirm(now time.Time, name string, conditions ...Condition) (Releaser, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
name = path.Clean("/" + name)
// TODO: touch n.held.
panic("TODO")
}
func (m *memLS) Create(now time.Time, details LockDetails) (string, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
name := path.Clean("/" + details.Root)
if !m.canCreate(name, details.Depth) {
return "", ErrLocked
}
n := m.create(name)
n.token = m.nextToken()
m.byToken[n.token] = n
n.details = details
// TODO: set n.expiry.
return n.token, nil
}
func (m *memLS) Refresh(now time.Time, token string, duration time.Duration) (LockDetails, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
n := m.byToken[token]
if n == nil {
return LockDetails{}, ErrNoSuchLock
}
if n.held {
return LockDetails{}, ErrLocked
}
n.details.Duration = duration
// TODO: update n.expiry.
return n.details, nil
}
// TODO: a MemLS implementation.
func (m *memLS) Unlock(now time.Time, token string) error {
m.mu.Lock()
defer m.mu.Unlock()
m.collectExpiredNodes(now)
n := m.byToken[token]
if n == nil {
return ErrNoSuchLock
}
if n.held {
return ErrLocked
}
m.remove(n)
return nil
}
func (m *memLS) canCreate(name string, depth int) bool {
return walkToRoot(name, func(name0 string, first bool) bool {
n := m.byName[name0]
if n == nil {
return true
}
if first {
if n.token != "" {
// The target node is already locked.
return false
}
if depth < 0 {
// The requested lock depth is infinite, and the fact that n exists
// (n != nil) means that a descendent of the target node is locked.
return false
}
} else if n.token != "" && n.details.Depth < 0 {
// An ancestor of the target node is locked with infinite depth.
return false
}
return true
})
}
func (m *memLS) create(name string) (ret *memLSNode) {
walkToRoot(name, func(name0 string, first bool) bool {
n := m.byName[name0]
if n == nil {
n = &memLSNode{
details: LockDetails{
Root: name0,
},
}
m.byName[name0] = n
}
n.refCount++
if first {
ret = n
}
return true
})
return ret
}
func (m *memLS) remove(n *memLSNode) {
delete(m.byToken, n.token)
n.token = ""
walkToRoot(n.details.Root, func(name0 string, first bool) bool {
x := m.byName[name0]
x.refCount--
if x.refCount == 0 {
delete(m.byName, name0)
}
return true
})
}
func walkToRoot(name string, f func(name0 string, first bool) bool) bool {
for first := true; ; first = false {
if !f(name, first) {
return false
}
if name == "/" {
break
}
name = name[:strings.LastIndex(name, "/")]
if name == "" {
name = "/"
}
}
return true
}
type memLSNode struct {
// details are the lock metadata. Even if this node's name is not explicitly locked,
// details.Root will still equal the node's name.
details LockDetails
// token is the unique identifier for this node's lock. An empty token means that
// this node is not explicitly locked.
token string
// refCount is the number of self-or-descendent nodes that are explicitly locked.
refCount int
// expiry is when this node's lock expires.
expiry time.Time
// held is whether this node's lock is actively held by a Confirm call.
held bool
}
// 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
import (
"fmt"
"math/rand"
"path"
"reflect"
"sort"
"strings"
"testing"
"time"
)
func TestWalkToRoot(t *testing.T) {
testCases := []struct {
name string
want []string
}{{
"/a/b/c/d",
[]string{
"/a/b/c/d",
"/a/b/c",
"/a/b",
"/a",
"/",
},
}, {
"/a",
[]string{
"/a",
"/",
},
}, {
"/",
[]string{
"/",
},
}}
for _, tc := range testCases {
var got []string
if !walkToRoot(tc.name, func(name0 string, first bool) bool {
if first != (len(got) == 0) {
t.Errorf("name=%q: first=%t but len(got)==%d", tc.name, first, len(got))
return false
}
got = append(got, name0)
return true
}) {
continue
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("name=%q:\ngot %q\nwant %q", tc.name, got, tc.want)
}
}
}
// lockTestNames are the names of a set of mutually compatible locks. For each
// name fragment:
// - _ means no explicit lock.
// - i means a infinite-depth lock,
// - z means a zero-depth lock,
var lockTestNames = []string{
"/_/_/_/_/z",
"/_/_/i",
"/_/z",
"/_/z/i",
"/_/z/z",
"/_/z/_/i",
"/_/z/_/z",
"/i",
"/z",
"/z/_/i",
"/z/_/z",
}
func lockTestDepth(name string) int {
switch name[len(name)-1] {
case 'i':
return -1
case 'z':
return 0
}
panic(fmt.Sprintf("lock name %q did not end with 'i' or 'z'", name))
}
func TestMemLSCanCreate(t *testing.T) {
now := time.Unix(0, 0)
m := NewMemLS().(*memLS)
for _, name := range lockTestNames {
_, err := m.Create(now, LockDetails{
Depth: lockTestDepth(name),
Duration: -1,
Root: name,
})
if err != nil {
t.Fatalf("creating lock for %q: %v", name, err)
}
}
wantCanCreate := func(name string, depth int) bool {
for _, n := range lockTestNames {
switch {
case n == name:
// An existing lock has the same name as the proposed lock.
return false
case strings.HasPrefix(n, name):
// An existing lock would be a child of the proposed lock,
// which conflicts if the proposed lock has infinite depth.
if depth < 0 {
return false
}
case strings.HasPrefix(name, n):
// An existing lock would be an ancestor of the proposed lock,
// which conflicts if the ancestor has infinite depth.
if n[len(n)-1] == 'i' {
return false
}
}
}
return true
}
var check func(int, string)
check = func(recursion int, name string) {
for _, depth := range []int{-1, 0} {
got := m.canCreate(name, depth)
want := wantCanCreate(name, depth)
if got != want {
t.Errorf("canCreate name=%q depth=%d: got %t, want %t", name, depth, got, want)
}
}
if recursion == 6 {
return
}
if name != "/" {
name += "/"
}
for _, c := range "_iz" {
check(recursion+1, name+string(c))
}
}
check(0, "/")
}
func TestMemLSCreateUnlock(t *testing.T) {
now := time.Unix(0, 0)
m := NewMemLS().(*memLS)
rng := rand.New(rand.NewSource(0))
tokens := map[string]string{}
for i := 0; i < 1000; i++ {
name := lockTestNames[rng.Intn(len(lockTestNames))]
if token := tokens[name]; token != "" {
if err := m.Unlock(now, token); err != nil {
t.Fatalf("iteration #%d: unlock %q: %v", i, name, err)
}
tokens[name] = ""
} else {
token, err := m.Create(now, LockDetails{
Depth: lockTestDepth(name),
Duration: -1,
Root: name,
})
if err != nil {
t.Fatalf("iteration #%d: create %q: %v", i, name, err)
}
tokens[name] = token
}
if err := m.consistent(); err != nil {
t.Fatalf("iteration #%d: inconsistent state: %v", i, err)
}
}
}
func (m *memLS) consistent() error {
m.mu.Lock()
defer m.mu.Unlock()
// If m.byName is non-empty, then it must contain an entry for the root "/",
// and its refCount should equal the number of locked nodes.
if len(m.byName) > 0 {
n := m.byName["/"]
if n == nil {
return fmt.Errorf(`non-empty m.byName does not contain the root "/"`)
}
if n.refCount != len(m.byToken) {
return fmt.Errorf("root node refCount=%d, differs from len(m.byToken)=%d", n.refCount, len(m.byToken))
}
}
for name, n := range m.byName {
// The map keys should be consistent with the node's copy of the key.
if n.details.Root != name {
return fmt.Errorf("node name %q != byName map key %q", n.details.Root, name)
}
// A name must be clean, and start with a "/".
if len(name) == 0 || name[0] != '/' {
return fmt.Errorf(`node name %q does not start with "/"`, name)
}
if name != path.Clean(name) {
return fmt.Errorf(`node name %q is not clean`, name)
}
// A node's refCount should be positive.
if n.refCount <= 0 {
return fmt.Errorf("non-positive refCount for node at name %q", name)
}
// A node's refCount should be the number of self-or-descendents that
// are locked (i.e. have a non-empty token).
var list []string
for name0, n0 := range m.byName {
// All of lockTestNames' name fragments are one byte long: '_', 'i' or 'z',
// so strings.HasPrefix is equivalent to self-or-descendent name match.
// We don't have to worry about "/foo/bar" being a false positive match
// for "/foo/b".
if strings.HasPrefix(name0, name) && n0.token != "" {
list = append(list, name0)
}
}
if n.refCount != len(list) {
sort.Strings(list)
return fmt.Errorf("node at name %q has refCount %d but locked self-or-descendents are %q (len=%d)",
name, n.refCount, list, len(list))
}
// A node n is in m.byToken if it has a non-empty token.
if n.token != "" {
if _, ok := m.byToken[n.token]; !ok {
return fmt.Errorf("node at name %q has token %q but not in m.byToken", name, n.token)
}
}
}
for token, n := range m.byToken {
// The map keys should be consistent with the node's copy of the key.
if n.token != token {
return fmt.Errorf("node token %q != byToken map key %q", n.token, token)
}
// Every node in m.byToken is in m.byName.
if _, ok := m.byName[n.details.Root]; !ok {
return fmt.Errorf("node at name %q in m.byToken but not in m.byName", n.details.Root)
}
}
return nil
}
......@@ -67,16 +67,14 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
}
}
type nopCloser struct{}
type nopReleaser struct{}
func (nopCloser) Close() error {
return nil
}
func (nopReleaser) Release() {}
func (h *Handler) confirmLocks(r *http.Request) (closer io.Closer, status int, err error) {
func (h *Handler) confirmLocks(r *http.Request) (releaser Releaser, status int, err error) {
hdr := r.Header.Get("If")
if hdr == "" {
return nopCloser{}, 0, nil
return nopReleaser{}, 0, nil
}
ih, ok := parseIfHeader(hdr)
if !ok {
......@@ -88,16 +86,16 @@ func (h *Handler) confirmLocks(r *http.Request) (closer io.Closer, status int, e
if path == "" {
path = r.URL.Path
}
closer, err = h.LockSystem.Confirm(path, l.conditions...)
releaser, err = h.LockSystem.Confirm(time.Now(), path, l.conditions...)
if err == ErrConfirmationFailed {
continue
}
if err != nil {
return nil, http.StatusInternalServerError, err
}
return closer, 0, nil
return releaser, 0, nil
}
return nil, http.StatusPreconditionFailed, errLocked
return nil, http.StatusPreconditionFailed, ErrLocked
}
func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status int, err error) {
......@@ -134,11 +132,11 @@ func (h *Handler) handleGetHeadPost(w http.ResponseWriter, r *http.Request) (sta
}
func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status int, err error) {
closer, status, err := h.confirmLocks(r)
releaser, status, err := h.confirmLocks(r)
if err != nil {
return status, err
}
defer closer.Close()
defer releaser.Release()
if err := h.FileSystem.RemoveAll(r.URL.Path); err != nil {
if os.IsNotExist(err) {
......@@ -151,11 +149,11 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i
}
func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int, err error) {
closer, status, err := h.confirmLocks(r)
releaser, status, err := h.confirmLocks(r)
if err != nil {
return status, err
}
defer closer.Close()
defer releaser.Release()
f, err := h.FileSystem.OpenFile(r.URL.Path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
if err != nil {
......@@ -169,11 +167,11 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
}
func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status int, err error) {
closer, status, err := h.confirmLocks(r)
releaser, status, err := h.confirmLocks(r)
if err != nil {
return status, err
}
defer closer.Close()
defer releaser.Release()
if r.ContentLength > 0 {
return http.StatusUnsupportedMediaType, nil
......@@ -197,7 +195,7 @@ func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus
return status, err
}
token, ld := "", LockDetails{}
token, ld, now := "", LockDetails{}, time.Now()
if li == (lockInfo{}) {
// An empty lockInfo means to refresh the lock.
ih, ok := parseIfHeader(r.Header.Get("If"))
......@@ -210,38 +208,42 @@ func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus
if token == "" {
return http.StatusBadRequest, errInvalidLockToken
}
var closer io.Closer
ld, closer, err = h.LockSystem.Refresh(token, time.Now(), duration)
ld, err = h.LockSystem.Refresh(now, token, 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
}
if depth > 0 {
// Section 9.10.3 says that "Values other than 0 or infinity must not be
// used with the Depth header on a LOCK method".
return http.StatusBadRequest, errInvalidDepth
}
ld = LockDetails{
Depth: depth,
Duration: duration,
OwnerXML: li.Owner.InnerXML,
Path: r.URL.Path,
Root: r.URL.Path,
}
var closer io.Closer
token, closer, err = h.LockSystem.Create(r.URL.Path, time.Now(), ld)
token, err = h.LockSystem.Create(now, ld)
if err != nil {
if err == ErrLocked {
return StatusLocked, err
}
return http.StatusInternalServerError, err
}
defer func() {
if retErr != nil {
h.LockSystem.Unlock(token)
h.LockSystem.Unlock(now, token)
}
}()
defer closer.Close()
// Create the resource if it didn't previously exist.
if _, err := h.FileSystem.Stat(r.URL.Path); err != nil {
......@@ -272,11 +274,13 @@ func (h *Handler) handleUnlock(w http.ResponseWriter, r *http.Request) (status i
}
t = t[1 : len(t)-1]
switch err = h.LockSystem.Unlock(t); err {
switch err = h.LockSystem.Unlock(time.Now(), t); err {
case nil:
return http.StatusNoContent, err
case ErrForbidden:
return http.StatusForbidden, err
case ErrLocked:
return StatusLocked, err
case ErrNoSuchLock:
return http.StatusConflict, err
default:
......@@ -320,10 +324,10 @@ func StatusText(code int) string {
}
var (
errInvalidDepth = errors.New("webdav: invalid depth")
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")
......
......@@ -79,7 +79,7 @@ func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
" <D:locktoken><D:href>%s</D:href></D:locktoken>\n"+
" <D:lockroot><D:href>%s</D:href></D:lockroot>\n"+
"</D:activelock></D:lockdiscovery></D:prop>",
depth, ld.OwnerXML, timeout, escape(token), escape(ld.Path),
depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root),
)
}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment