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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Ifheader field