1
mirror of https://github.com/rclone/rclone synced 2024-11-23 00:06:55 +01:00

serve webdav: implement owncloud checksum and modtime extensions

* implement owncloud checksum and modtime extensions for webdav server
* test rclone webdav server as owncloud webdav
This commit is contained in:
WeidiDeng 2023-05-15 22:38:00 +08:00 committed by GitHub
parent 1f887f7ba0
commit ceb9406c2f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 117 additions and 4 deletions

View File

@ -3,10 +3,12 @@ package webdav
import (
"context"
"encoding/xml"
"errors"
"fmt"
"net/http"
"os"
"strconv"
"strings"
"time"
@ -255,6 +257,52 @@ func (w *WebDAV) auth(user, pass string) (value interface{}, err error) {
return VFS, err
}
type webdavRW struct {
http.ResponseWriter
status int
}
func (rw *webdavRW) WriteHeader(statusCode int) {
rw.status = statusCode
rw.ResponseWriter.WriteHeader(statusCode)
}
func (rw *webdavRW) isSuccessfull() bool {
return rw.status == 0 || (rw.status >= 200 && rw.status <= 299)
}
func (w *WebDAV) postprocess(r *http.Request, remote string) {
// set modtime from requests, don't write to client because status is already written
switch r.Method {
case "COPY", "MOVE", "PUT":
VFS, err := w.getVFS(r.Context())
if err != nil {
fs.Errorf(nil, "Failed to get VFS: %v", err)
return
}
// Get the node
node, err := VFS.Stat(remote)
if err != nil {
fs.Errorf(nil, "Failed to stat node: %v", err)
return
}
mh := r.Header.Get("X-OC-Mtime")
if mh != "" {
modtimeUnix, err := strconv.ParseInt(mh, 10, 64)
if err == nil {
err = node.SetModTime(time.Unix(modtimeUnix, 0))
if err != nil {
fs.Errorf(nil, "Failed to set modtime: %v", err)
}
} else {
fs.Errorf(nil, "Failed to parse modtime: %v", err)
}
}
}
}
func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
urlPath := r.URL.Path
isDir := strings.HasSuffix(urlPath, "/")
@ -266,7 +314,12 @@ func (w *WebDAV) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
// Add URL Prefix back to path since webdavhandler needs to
// return absolute references.
r.URL.Path = w.opt.HTTP.BaseURL + r.URL.Path
w.webdavhandler.ServeHTTP(rw, r)
wrw := &webdavRW{ResponseWriter: rw}
w.webdavhandler.ServeHTTP(wrw, r)
if wrw.isSuccessfull() {
w.postprocess(r, remote)
}
}
// serveDir serves a directory index at dirRemote
@ -356,7 +409,7 @@ func (w *WebDAV) OpenFile(ctx context.Context, name string, flags int, perm os.F
if err != nil {
return nil, err
}
return Handle{Handle: f, w: w}, nil
return Handle{Handle: f, w: w, ctx: ctx}, nil
}
// RemoveAll removes a file or a directory and its contents
@ -404,7 +457,8 @@ func (w *WebDAV) Stat(ctx context.Context, name string) (fi os.FileInfo, err err
// Handle represents an open file
type Handle struct {
vfs.Handle
w *WebDAV
w *WebDAV
ctx context.Context
}
// Readdir reads directory entries from the handle
@ -429,6 +483,65 @@ func (h Handle) Stat() (fi os.FileInfo, err error) {
return FileInfo{FileInfo: fi, w: h.w}, nil
}
// DeadProps returns extra properties about the handle
func (h Handle) DeadProps() (map[xml.Name]webdav.Property, error) {
var (
xmlName xml.Name
property webdav.Property
properties = make(map[xml.Name]webdav.Property)
)
if h.w.opt.HashType != hash.None {
entry := h.Handle.Node().DirEntry()
if o, ok := entry.(fs.Object); ok {
hash, err := o.Hash(h.ctx, h.w.opt.HashType)
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, strings.ToUpper(h.w.opt.HashType.String())...)
property.InnerXML = append(property.InnerXML, ':')
property.InnerXML = append(property.InnerXML, hash...)
property.InnerXML = append(property.InnerXML, "</checksum>"...)
properties[xmlName] = property
} else {
fs.Errorf(nil, "failed to calculate hash: %v", err)
}
}
}
xmlName.Space = "DAV:"
xmlName.Local = "lastmodified"
property.XMLName = xmlName
property.InnerXML = strconv.AppendInt(nil, h.Handle.Node().ModTime().Unix(), 10)
properties[xmlName] = property
return properties, nil
}
// Patch changes modtime of the underlying resources, it returns ok for all properties, the error is from setModtime if any
// FIXME does not check for invalid property and SetModTime error
func (h Handle) Patch(proppatches []webdav.Proppatch) ([]webdav.Propstat, error) {
var (
stat webdav.Propstat
err error
)
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 == "DAV:" && prop.XMLName.Local == "lastmodified" {
var modtimeUnix int64
modtimeUnix, err = strconv.ParseInt(string(prop.InnerXML), 10, 64)
if err == nil {
err = h.Handle.Node().SetModTime(time.Unix(modtimeUnix, 0))
}
}
}
}
return []webdav.Propstat{stat}, err
}
// FileInfo represents info about a file satisfying os.FileInfo and
// also some additional interfaces for webdav for ETag and ContentType
type FileInfo struct {

View File

@ -65,7 +65,7 @@ func TestWebDav(t *testing.T) {
// Config for the backend we'll use to connect to the server
config := configmap.Simple{
"type": "webdav",
"vendor": "other",
"vendor": "owncloud",
"url": w.Server.URLs()[0],
"user": testUser,
"pass": obscure.MustObscure(testPass),