-
Notifications
You must be signed in to change notification settings - Fork 97
webdav: add support for locks #187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
|
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. 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 |
Ifheader field