note
This article was last updated on May 29, 2023, 2 years ago. The content may be out of date.
Golang comes with a webdav server implementation. It only supports webdav operations and nothing more. To manager a group of users, we need to implement our own handling logic.
hacdias’s webdav server associates each user with his/her own webdav.Handler
, and each authenticated request goes to their respective handler. rclone can generate dynamic webdav backends using auth proxy
. By implementing auth proxy, rclone can serve different contents based on users.
However, both of them suffer from lock states not shared, meaning when multiple users share the same directory, some changes may be lost if they try to modify its resources concurrently.
How to Solve this Problem?
Looking at webdav.Handler
struct, we can see LockSystem
which handles lock states, doesn’t have methods that have a parameter of type context.Context
. This means we can’t differentiate between requests like FileSystem
does by embedding per-user information in the context
. However, although less efficient, we can create new webdav.FileSystem
and webdav.Handler
for each request.
Creating a New LockSystem
This LockSystem
type we’re creating will delegate all lock handling to a shared underlying LockSystem
. It will have a scope
field to determine from which directory the full path of the resource is calculated. First we take a look at webdav.memLS
, which is the underlying LockSystem
we’ll be using.
How memLS
works
Confirm
will callslashClean
to non-empty names and then determine if the call succeeds.Create
will callslashClean
toLockDetails
’sRoot
field and use it instead.Refresh
will returnLockDetails
if one is created byCreate
.Unlock
only checks the lock token and remove the lock if present.
As we can see, for all methods except Unlock
we’ll need to modify the parameter or results in some way to match webdav.memLS
’s behavior for a user with a specific root directory.
Adapting LockSystem Behavior
First we’ll need to normalize the root path because we’ll use it to modify LockDetails
passed to or returned from underlying webdav.memLS
. Just copy the slashClean
function:
func slashClean(name string) string {
if name == "" || name[0] != '/' {
name = "/" + name
}
return path.Clean(name)
}
And use it to normalize the root path, so it always starts with a slash and ends without a slash unless the path is just a single slash.
Only Confirm
, Create
, Refresh
needs to be reimplemented to deal with the root.
Confirm
We modify every non-empty names by joining them with the root, and pass the new names to the underlying LockSystem
:
func (l *scopedLS) Confirm(now time.Time, name0, name1 string, conditions ...webdav.Condition) (release func(), err error) {
if name0 != "" {
name0 = path.Join(l.scope, name0)
}
if name1 != "" {
name1 = path.Join(l.scope, name1)
}
return l.LockSystem.Confirm(now, name0, name1, conditions...)
}
Create
Change the Root
field of LockDetails
struct by joining it with the root, and pass the new LockDetails
to the underlying LockSystem
:
func (l *scopedLS) Create(now time.Time, details webdav.LockDetails) (token string, err error) {
details.Root = path.Join(l.scope, details.Root)
return l.LockSystem.Create(now, details)
}
Refresh
First call the underlying LockSystem
’s Refresh
method, and if there is no error and the root is not slash, remove the root as a prefix of the Root
field of the returned LockDetails
and then return it.
func (l *scopedLS) Refresh(now time.Time, token string, duration time.Duration) (webdav.LockDetails, error) {
details, err := l.LockSystem.Refresh(now, token, duration)
if err != nil {
return details, err
}
if l.scope != "/" {
details.Root = strings.TrimPrefix(details.Root, l.scope)
}
if details.Root == "" {
details.Root = "/"
}
return details, nil
}
Because slashClean
will make sure path starts with a slash and ends without a slash unless the path is a single slash, the modification done here is in line with webdav.memLS
’s implementation.