go-webdav
go-webdav copied to clipboard
webdav: add support for locks
- [x] Decode/encode LOCK/UNLOCK requests
- [x] Decode
Ifheader field - [ ] Plumb all relevant endpoints
- [ ] Add in-memory lock system implementation
Hi, I've done some work based on this branch, adding a memory-based Lock implementation and enabling Lock support in other parts of the code. This work allowed me to connect to the sample server via Mac's Finder, but I haven't done further testing.
I really like this library, so I've been looking forward to this PR for a long time, hoping to use it for my scenario.
Here's my patch, and I hope it helps advance the work on this branch.
Subject: [PATCH] Add in-memory WebDAV locking system with support for LOCK/UNLOCK
---
Index: caldav/server.go
===================================================================
diff --git a/caldav/server.go b/caldav/server.go
--- a/caldav/server.go (revision e933509518d29927d6b0a78abfa9d2f02db97cb8)
+++ b/caldav/server.go (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
@@ -317,26 +317,39 @@
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
caps = []string{"calendar-access"}
+ // Add lock capability if global lock system is available
+ if webdav.GetGlobalLockSystem() != nil {
+ caps = append(caps, "1")
+ }
+
+ var methods []string
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeCalendarObject {
- return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil
- }
-
- var dataReq CalendarCompRequest
- _, err = b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
- if httpErr, ok := err.(*internal.HTTPError); ok && httpErr.Code == http.StatusNotFound {
- return caps, []string{http.MethodOptions, http.MethodPut}, nil
- } else if err != nil {
- return nil, nil, err
- }
-
- return caps, []string{
- http.MethodOptions,
- http.MethodHead,
- http.MethodGet,
- http.MethodPut,
- http.MethodDelete,
- "PROPFIND",
- }, nil
+ methods = []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}
+ } else {
+ var dataReq CalendarCompRequest
+ _, err = b.Backend.GetCalendarObject(r.Context(), r.URL.Path, &dataReq)
+ if httpErr, ok := err.(*internal.HTTPError); ok && httpErr.Code == http.StatusNotFound {
+ methods = []string{http.MethodOptions, http.MethodPut}
+ } else if err != nil {
+ return nil, nil, err
+ } else {
+ methods = []string{
+ http.MethodOptions,
+ http.MethodHead,
+ http.MethodGet,
+ http.MethodPut,
+ http.MethodDelete,
+ "PROPFIND",
+ }
+ }
+ }
+
+ // Add lock methods if global lock system is available
+ if webdav.GetGlobalLockSystem() != nil {
+ methods = append(methods, "LOCK", "UNLOCK")
+ }
+
+ return caps, methods, nil
}
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
@@ -742,11 +755,13 @@
}
func (b *backend) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (lock *internal.Lock, created bool, err error) {
- return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "caldav: unsupported method")
+ // Use the global lock system
+ return webdav.GetGlobalLockSystem().Lock(r, depth, timeout, refreshToken)
}
func (b *backend) Unlock(r *http.Request, tokenHref string) error {
- return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
+ // Use the global lock system
+ return webdav.GetGlobalLockSystem().Unlock(r, tokenHref)
}
// https://datatracker.ietf.org/doc/html/rfc4791#section-5.3.2.1
Index: carddav/server.go
===================================================================
diff --git a/carddav/server.go b/carddav/server.go
--- a/carddav/server.go (revision e933509518d29927d6b0a78abfa9d2f02db97cb8)
+++ b/carddav/server.go (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
@@ -282,28 +282,41 @@
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
caps = []string{"addressbook"}
+ // Add lock capability if global lock system is available
+ if webdav.GetGlobalLockSystem() != nil {
+ caps = append(caps, "1")
+ }
+
+ var methods []string
if b.resourceTypeAtPath(r.URL.Path) != resourceTypeAddressObject {
// Note: some clients assume the address book is read-only when
// DELETE/MKCOL are missing
- return caps, []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}, nil
- }
-
- var dataReq AddressDataRequest
- _, err = b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
- if httpErr, ok := err.(*internal.HTTPError); ok && httpErr.Code == http.StatusNotFound {
- return caps, []string{http.MethodOptions, http.MethodPut}, nil
- } else if err != nil {
- return nil, nil, err
- }
-
- return caps, []string{
- http.MethodOptions,
- http.MethodHead,
- http.MethodGet,
- http.MethodPut,
- http.MethodDelete,
- "PROPFIND",
- }, nil
+ methods = []string{http.MethodOptions, "PROPFIND", "REPORT", "DELETE", "MKCOL"}
+ } else {
+ var dataReq AddressDataRequest
+ _, err = b.Backend.GetAddressObject(r.Context(), r.URL.Path, &dataReq)
+ if httpErr, ok := err.(*internal.HTTPError); ok && httpErr.Code == http.StatusNotFound {
+ methods = []string{http.MethodOptions, http.MethodPut}
+ } else if err != nil {
+ return nil, nil, err
+ } else {
+ methods = []string{
+ http.MethodOptions,
+ http.MethodHead,
+ http.MethodGet,
+ http.MethodPut,
+ http.MethodDelete,
+ "PROPFIND",
+ }
+ }
+ }
+
+ // Add lock methods if global lock system is available
+ if webdav.GetGlobalLockSystem() != nil {
+ methods = append(methods, "LOCK", "UNLOCK")
+ }
+
+ return caps, methods, nil
}
func (b *backend) HeadGet(w http.ResponseWriter, r *http.Request) error {
@@ -735,11 +748,13 @@
}
func (b *backend) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (lock *internal.Lock, created bool, err error) {
- return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "carddav: unsupported method")
+ // Use the global lock system
+ return webdav.GetGlobalLockSystem().Lock(r, depth, timeout, refreshToken)
}
func (b *backend) Unlock(r *http.Request, tokenHref string) error {
- return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
+ // Use the global lock system
+ return webdav.GetGlobalLockSystem().Unlock(r, tokenHref)
}
// PreconditionType as defined in https://tools.ietf.org/rfcmarkup?doc=6352#section-6.3.2.1
Index: internal/elements.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/internal/elements.go b/internal/elements.go
--- a/internal/elements.go (revision e933509518d29927d6b0a78abfa9d2f02db97cb8)
+++ b/internal/elements.go (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
@@ -21,6 +21,7 @@
GetLastModifiedName = xml.Name{Namespace, "getlastmodified"}
GetETagName = xml.Name{Namespace, "getetag"}
SupportedLockName = xml.Name{Namespace, "supportedlock"}
+ LockDiscoveryName = xml.Name{Namespace, "lockdiscovery"}
CurrentUserPrincipalName = xml.Name{Namespace, "current-user-principal"}
)
Index: locks.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/locks.go b/locks.go
new file mode 100644
--- /dev/null (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
+++ b/locks.go (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
@@ -0,0 +1,168 @@
+package webdav
+
+import (
+ "fmt"
+ "net/http"
+ "sync"
+ "time"
+
+ "github.com/emersion/go-webdav/internal"
+)
+
+// LockSystem provides an in-memory implementation of WebDAV locks.
+type LockSystem struct {
+ mu sync.RWMutex
+ locks map[string]*lockInfo // Map of token -> lock info
+ paths map[string][]string // Map of path -> tokens
+}
+
+// lockInfo contains information about an active lock.
+type lockInfo struct {
+ Token string
+ Root string
+ Created time.Time
+ Timeout time.Duration
+}
+
+// Global lock system that can be used by all backends
+var globalLockSystem *LockSystem
+
+// NewLockSystem creates a new in-memory lock system.
+func NewLockSystem() *LockSystem {
+ return &LockSystem{
+ locks: make(map[string]*lockInfo),
+ paths: make(map[string][]string),
+ }
+}
+
+// GetGlobalLockSystem returns the global lock system, creating it if necessary.
+func GetGlobalLockSystem() *LockSystem {
+ if globalLockSystem == nil {
+ globalLockSystem = NewLockSystem()
+ }
+ return globalLockSystem
+}
+
+// Lock creates or refreshes a lock.
+func (ls *LockSystem) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (*internal.Lock, bool, error) {
+ ls.mu.Lock()
+ defer ls.mu.Unlock()
+
+ path := r.URL.Path
+
+ // If refreshToken is provided, refresh the existing lock
+ if refreshToken != "" {
+ lock, ok := ls.locks[refreshToken]
+ if !ok {
+ return nil, false, internal.HTTPErrorf(http.StatusPreconditionFailed, "webdav: lock token not found")
+ }
+
+ // Update the timeout
+ lock.Timeout = timeout
+ lock.Created = time.Now()
+
+ return &internal.Lock{
+ Href: lock.Token,
+ Root: lock.Root,
+ Timeout: lock.Timeout,
+ }, false, nil
+ }
+
+ // Check if the path is already locked
+ if tokens, ok := ls.paths[path]; ok && len(tokens) > 0 {
+ return nil, false, internal.HTTPErrorf(http.StatusLocked, "webdav: path already locked")
+ }
+
+ // Create a new lock
+ token := generateToken()
+ lock := &lockInfo{
+ Token: token,
+ Root: path,
+ Created: time.Now(),
+ Timeout: timeout,
+ }
+
+ // Store the lock
+ ls.locks[token] = lock
+ ls.paths[path] = append(ls.paths[path], token)
+
+ return &internal.Lock{
+ Href: token,
+ Root: path,
+ Timeout: timeout,
+ }, true, nil
+}
+
+// Unlock removes a lock.
+func (ls *LockSystem) Unlock(r *http.Request, tokenHref string) error {
+ ls.mu.Lock()
+ defer ls.mu.Unlock()
+
+ lock, ok := ls.locks[tokenHref]
+ if !ok {
+ return internal.HTTPErrorf(http.StatusPreconditionFailed, "webdav: lock token not found")
+ }
+
+ // Remove the lock from the paths map
+ path := lock.Root
+ tokens := ls.paths[path]
+ for i, t := range tokens {
+ if t == tokenHref {
+ // Remove the token from the slice
+ ls.paths[path] = append(tokens[:i], tokens[i+1:]...)
+ break
+ }
+ }
+
+ // If the path has no more locks, remove it from the map
+ if len(ls.paths[path]) == 0 {
+ delete(ls.paths, path)
+ }
+
+ // Remove the lock from the locks map
+ delete(ls.locks, tokenHref)
+
+ return nil
+}
+
+// CleanExpiredLocks removes expired locks.
+func (ls *LockSystem) CleanExpiredLocks() {
+ ls.mu.Lock()
+ defer ls.mu.Unlock()
+
+ now := time.Now()
+ for token, lock := range ls.locks {
+ // Skip infinite locks
+ if lock.Timeout == 0 {
+ continue
+ }
+
+ // Check if the lock has expired
+ if now.Sub(lock.Created) > lock.Timeout {
+ // Remove the lock from the paths map
+ path := lock.Root
+ tokens := ls.paths[path]
+ for i, t := range tokens {
+ if t == token {
+ // Remove the token from the slice
+ ls.paths[path] = append(tokens[:i], tokens[i+1:]...)
+ break
+ }
+ }
+
+ // If the path has no more locks, remove it from the map
+ if len(ls.paths[path]) == 0 {
+ delete(ls.paths, path)
+ }
+
+ // Remove the lock from the locks map
+ delete(ls.locks, token)
+ }
+ }
+}
+
+// generateToken creates a unique token for a lock.
+func generateToken() string {
+ // Create a simple unique token using timestamp and random number
+ return fmt.Sprintf("opaquelocktoken:%d-%d", time.Now().UnixNano(), time.Now().Unix())
+}
Index: server.go
IDEA additional info:
Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP
<+>UTF-8
===================================================================
diff --git a/server.go b/server.go
--- a/server.go (revision e933509518d29927d6b0a78abfa9d2f02db97cb8)
+++ b/server.go (revision 72a787ada657475be77b0efd5c5bc9cdced67961)
@@ -29,6 +29,7 @@
// server.
type Handler struct {
FileSystem FileSystem
+ LockSystem *LockSystem
}
// ServeHTTP implements http.Handler.
@@ -38,7 +39,15 @@
return
}
- b := backend{h.FileSystem}
+ // Use the global lock system if not provided
+ if h.LockSystem == nil {
+ h.LockSystem = GetGlobalLockSystem()
+ }
+
+ b := backend{
+ FileSystem: h.FileSystem,
+ LockSystem: h.LockSystem,
+ }
hh := internal.Handler{Backend: &b}
hh.ServeHTTP(w, r)
}
@@ -54,14 +63,23 @@
type backend struct {
FileSystem FileSystem
+ LockSystem *LockSystem
}
func (b *backend) Options(r *http.Request) (caps []string, allow []string, err error) {
+ // Add lock capability if lock system is available
caps = []string{"2"}
+ if b.LockSystem != nil {
+ caps = append(caps, "1")
+ }
fi, err := b.FileSystem.Stat(r.Context(), r.URL.Path)
if internal.IsNotFound(err) {
- return caps, []string{http.MethodOptions, http.MethodPut, "MKCOL"}, nil
+ methods := []string{http.MethodOptions, http.MethodPut, "MKCOL"}
+ if b.LockSystem != nil {
+ methods = append(methods, "LOCK")
+ }
+ return caps, methods, nil
} else if err != nil {
return nil, nil, err
}
@@ -78,6 +96,11 @@
allow = append(allow, http.MethodHead, http.MethodGet, http.MethodPut)
}
+ // Add lock methods if lock system is available
+ if b.LockSystem != nil {
+ allow = append(allow, "LOCK", "UNLOCK")
+ }
+
return caps, allow, nil
}
@@ -171,6 +194,12 @@
}},
})
+ // Add empty lockdiscovery property when lock system is available
+ // Actual lock information would be added by the lock system if needed
+ if b.LockSystem != nil {
+ props[internal.LockDiscoveryName] = internal.PropFindValue(&internal.LockDiscovery{})
+ }
+
if !fi.IsDir {
props[internal.GetContentLengthName] = internal.PropFindValue(&internal.GetContentLength{
Length: fi.Size,
@@ -281,11 +310,17 @@
}
func (b *backend) Lock(r *http.Request, depth internal.Depth, timeout time.Duration, refreshToken string) (lock *internal.Lock, created bool, err error) {
- return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
+ if b.LockSystem == nil {
+ return nil, false, internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: lock system not available")
+ }
+ return b.LockSystem.Lock(r, depth, timeout, refreshToken)
}
func (b *backend) Unlock(r *http.Request, tokenHref string) error {
- return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: unsupported method")
+ if b.LockSystem == nil {
+ return internal.HTTPErrorf(http.StatusMethodNotAllowed, "webdav: lock system not available")
+ }
+ return b.LockSystem.Unlock(r, tokenHref)
}
// BackendSuppliedHomeSet represents either a CalDAV calendar-home-set or a