note

This article was last updated on April 26, 2023, 1 year ago. The content may be out of date.

Golang x/net has an implementation of WebDAV protocol, which can be used as a backend for Rclone. Rclone can be used to manage files on various storage systems, be it local or on cloud. Using WebDAV with Rclone, we can sync local files with files stored on remote server.

However, vanilla golang webdav server doesn’t support checksum, meaning rclone will only use file size to determine if file has changed. In some situations, rclone won’t be able to pick up local filesystem changes, resulting in data discrepancy.

sequenceDiagram participant Local participant Remote Local->>Remote: Push Local Files loop Local Changes Local->>Local: Fixing Document Typos, Sizes Don't Change end Local->>Remote: Push Changes Note right of Local: Some Changes Not Pushed! Note over Local,Remote: Data Discrepancy

Fortunately, rclone supports several quirks other webdav servers have, such as checksum and modified time support for OwnCloud and NextCloud.

Extending Golang Webdav

When instantiating a new webdav handler instance, go allows us to use a custom filesystem implementation. What we are interested in is DeadPropsHolder interface. This interface allows a webdav resource to return custom properties and modify them. As this resource is returned from webdav filesystem interface, we need to create a custom filesystem type as well.

erDiagram FileSystem ||--o{ File : OpenFile File ||--o{ Property : DeadProps File ||--o{ Property : Patch

How RClone Interacts with WebDAV

Inspecting RClone source code can reveal rclone attaches X-OC-Mtime header for PUT, COPY and MOVE requests, and OC-Checksum for PUT requests.

Storing Checksums

For simplicity’s sake, we store the checksums as an extended attribute of the file itself. Extended attributes are often used to provide additional functionality to a filesystem. There is a golang library to manipulate the extended attributes of a file.

info

This library only supports linux, darwin, freebsd, netbsd, solaris as of now.

Implementing Webdav Server with Checksum Support

Now that we understand how webdav server can be extended and how rclone interacts with owncloud webdav servers and decided how to store these checksums, we can then implement these methods.

Our custom types will embed corresponding webdav types anonymously so that we can focus on methods that needs to be modified.

classDiagram class davFS{ webdav.Dir } class davFile{ webdav.File name string }

davFS

davFS is the filesystem implementation, responsible for returning file resources.

OpenFile

Because our resources need to implement DeadPropsHolder interface, we’ll need to rewrite this method to return our custom file implementation.

func (fs davFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
	file, err := fs.Dir.OpenFile(ctx, name, flag, perm)
	if err != nil {
		return nil, err
	}
	return &davFile{file, name}, nil
}

davFile

davFile is the actual resource the filesystem returns. It implements DeadPropsHolder interface. Because we delay the extraction of checksums, we have to save the resource path.

DeadProps

DeadProps returns the checksum of the file if found, the error is always nil.

func (f *davFile) DeadProps() (map[xml.Name]webdav.Property, error) {
	var (
		xmlName       xml.Name
		property      webdav.Property
		properties    = make(map[xml.Name]webdav.Property)
		checksum, err = readChecksum(f.name)
	)
	if err == nil {
		xmlName.Space = "http://owncloud.org/ns"
		xmlName.Local = "checksums"
		property.XMLName = xmlName
		property.InnerXML = append(property.InnerXML, "<checksum xmlns=\"http://owncloud.org/ns\">"...)
		property.InnerXML = append(property.InnerXML, checksum...)
		property.InnerXML = append(property.InnerXML, "</checksum>"...)
		properties[xmlName] = property
	}

	var stat fs.FileInfo
	stat, err = f.Stat()
	if err == nil {
		xmlName.Space = "DAV:"
		xmlName.Local = "lastmodified"
		property.XMLName = xmlName
		property.InnerXML = strconv.AppendInt(nil, stat.ModTime().Unix(), 10)
		properties[xmlName] = property
	}

	return properties, nil
}

tip

In addition to the checksum, we also return the modified time of the resource as a custom property. This property is what owncloud and nextcloud use when updating the modified time of a resource.

readChecksum is a util function that extracts the checksum of the resource.

Patch

PATCH applies property modification to the resource. It is used under the hood during copy operations. Note we abuse golang xml to decode the inner xml to []byte:

func (f *davFile) Patch(proppatches []webdav.Proppatch) ([]webdav.Propstat, error) {
	var stat webdav.Propstat
	stat.Status = http.StatusOK
	for _, patch := range proppatches {
		for _, prop := range patch.Props {
			stat.Props = append(stat.Props, webdav.Property{XMLName: prop.XMLName})
			if prop.XMLName.Space == "http://owncloud.org/ns" && prop.XMLName.Local == "checksums" {
				var data []byte
				_ = xml.Unmarshal(prop.InnerXML, &data)
				if len(data) != 0 {
					updateChecksum(f.name, data)
				}
			} else if prop.XMLName.Space == "DAV:" && prop.XMLName.Local == "lastmodified" {
				modtimeUnix, err := strconv.ParseInt(string(prop.InnerXML), 10, 64)
				if err == nil {
					updateModtime(f.name, time.Unix(modtimeUnix, 0))
				}
			}
		}
	}
	return []webdav.Propstat{stat}, nil
}

updateChecksum is a util function that updates the checksum of a file. updateModtime is a util function that updates the modified time of the file.

To Be Continued

This part only covers how rclone interacts with owncloud webdav server and webdav optional interface implementation. The next part deals with how to extract the extra information from http headers and some caveats with the integration.