This article was last updated on May 29, 2023, 1 year 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.

classDiagram class scopedLS { scope string webdav.LockSystem }

How memLS works

  1. Confirm will call slashClean to non-empty names and then determine if the call succeeds.
  2. Create will call slashClean to LockDetails’s Root field and use it instead.
  3. Refresh will return LockDetails if one is created by Create.
  4. 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.


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...)


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)


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.