diff --git a/internal/db/db.go b/internal/db/db.go
index 96529c15d..59e99f397 100644
--- a/internal/db/db.go
+++ b/internal/db/db.go
@@ -12,7 +12,7 @@ var db *gorm.DB
func Init(d *gorm.DB) {
db = d
- err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB))
+ err := AutoMigrate(new(model.Storage), new(model.User), new(model.Meta), new(model.SettingItem), new(model.SearchNode), new(model.TaskItem), new(model.SSHPublicKey), new(model.SharingDB), new(model.WebDAVProperty))
if err != nil {
log.Fatalf("failed migrate database: %s", err.Error())
}
diff --git a/internal/fs/copy_move.go b/internal/fs/copy_move.go
index e78fc9be8..3048fd38a 100644
--- a/internal/fs/copy_move.go
+++ b/internal/fs/copy_move.go
@@ -100,7 +100,7 @@ func (t *FileTransferTask) SetRetry(retry int, maxRetry int) {
}
}
-func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
+func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath, dstName string, skipHook ...bool) (task.TaskExtensionInfo, error) {
srcStorage, srcObjActualPath, err := op.GetStorageAndActualPath(srcObjPath)
if err != nil {
return nil, errors.WithMessage(err, "failed get src storage")
@@ -117,6 +117,12 @@ func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath str
if taskType == copy || taskType == merge {
err = op.Copy(ctx, srcStorage, srcObjActualPath, dstDirActualPath)
if !errors.Is(err, errs.NotImplement) && !errors.Is(err, errs.NotSupport) {
+ if err == nil && dstName != "" {
+ srcObjName := stdpath.Base(srcObjActualPath)
+ if srcObjName != dstName {
+ err = op.Rename(ctx, srcStorage, stdpath.Join(dstDirActualPath, srcObjName), dstName)
+ }
+ }
return nil, err
}
} else {
@@ -134,6 +140,7 @@ func transfer(ctx context.Context, taskType taskType, srcObjPath, dstDirPath str
DstStorage: dstStorage,
SrcActualPath: srcObjActualPath,
DstActualPath: dstDirActualPath,
+ DstName: dstName,
SrcStorageMp: srcStorage.GetStorage().MountPath,
DstStorageMp: dstStorage.GetStorage().MountPath,
},
@@ -189,9 +196,14 @@ func (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransfer
if err != nil {
return errors.WithMessagef(err, "failed list src [%s] objs", t.SrcActualPath)
}
- dstActualPath := stdpath.Join(t.DstActualPath, srcObj.GetName())
- task_group.TransferCoordinator.AppendPayload(t.groupID, task_group.DstPathToHook(dstActualPath))
-
+ dstName := srcObj.GetName()
+ if t.DstName != "" {
+ dstName = t.DstName
+ }
+ dstActualPath := stdpath.Join(t.DstActualPath, dstName)
+ if err := op.MakeDir(t.Ctx(), t.DstStorage, dstActualPath); err != nil {
+ return errors.WithMessagef(err, "failed create dst dir [%s]", dstActualPath)
+ }
existedObjs := make(map[string]bool)
if t.TaskType == merge {
dstObjs, err := op.List(t.Ctx(), t.DstStorage, dstActualPath, model.ListArgs{})
@@ -250,8 +262,12 @@ func (t *FileTransferTask) RunWithNextTaskCallback(f func(nextTask *FileTransfer
return errors.WithMessagef(err, "failed get [%s] link", t.SrcActualPath)
}
// any link provided is seekable
+ streamObj := srcObj
+ if t.DstName != "" {
+ streamObj = &model.ObjWrapName{Name: t.DstName, Obj: srcObj}
+ }
ss, err := stream.NewSeekableStream(&stream.FileStream{
- Obj: srcObj,
+ Obj: streamObj,
Ctx: t.Ctx(),
}, link)
if err != nil {
diff --git a/internal/fs/fs.go b/internal/fs/fs.go
index 67a1ac065..b7f064ac7 100644
--- a/internal/fs/fs.go
+++ b/internal/fs/fs.go
@@ -69,7 +69,7 @@ func MakeDir(ctx context.Context, path string) error {
}
func Move(ctx context.Context, srcPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
- req, err := transfer(ctx, move, srcPath, dstDirPath, skipHook...)
+ req, err := transfer(ctx, move, srcPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed move %s to %s: %+v", srcPath, dstDirPath, err)
}
@@ -77,15 +77,24 @@ func Move(ctx context.Context, srcPath, dstDirPath string, skipHook ...bool) (ta
}
func Copy(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
- res, err := transfer(ctx, copy, srcObjPath, dstDirPath, skipHook...)
+ res, err := transfer(ctx, copy, srcObjPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed copy %s to %s: %+v", srcObjPath, dstDirPath, err)
}
return res, err
}
+// CopyTo copies a file or directory to dstDirPath using dstName as its name.
+func CopyTo(ctx context.Context, srcObjPath, dstDirPath, dstName string, skipHook ...bool) (task.TaskExtensionInfo, error) {
+ res, err := transfer(ctx, copy, srcObjPath, dstDirPath, dstName, skipHook...)
+ if err != nil {
+ log.Errorf("failed copy %s to %s as %s: %+v", srcObjPath, dstDirPath, dstName, err)
+ }
+ return res, err
+}
+
func Merge(ctx context.Context, srcObjPath, dstDirPath string, skipHook ...bool) (task.TaskExtensionInfo, error) {
- res, err := transfer(ctx, merge, srcObjPath, dstDirPath, skipHook...)
+ res, err := transfer(ctx, merge, srcObjPath, dstDirPath, "", skipHook...)
if err != nil {
log.Errorf("failed merge %s to %s: %+v", srcObjPath, dstDirPath, err)
}
diff --git a/internal/fs/other.go b/internal/fs/other.go
index a23beb73b..a74eff412 100644
--- a/internal/fs/other.go
+++ b/internal/fs/other.go
@@ -53,6 +53,7 @@ type TaskData struct {
Status string `json:"-"` //don't save status to save space
SrcActualPath string `json:"src_path"`
DstActualPath string `json:"dst_path"`
+ DstName string `json:"dst_name,omitempty"`
SrcStorage driver.Driver `json:"-"`
DstStorage driver.Driver `json:"-"`
SrcStorageMp string `json:"src_storage_mp"`
diff --git a/internal/model/webdav_property.go b/internal/model/webdav_property.go
new file mode 100644
index 000000000..cdfedb9e7
--- /dev/null
+++ b/internal/model/webdav_property.go
@@ -0,0 +1,11 @@
+package model
+
+// WebDAVProperty stores a dead WebDAV property for a resource.
+type WebDAVProperty struct {
+ ID uint `json:"id" gorm:"primaryKey"`
+ Path string `json:"path" gorm:"uniqueIndex:idx_webdav_property"`
+ Namespace string `json:"namespace" gorm:"uniqueIndex:idx_webdav_property"`
+ Name string `json:"name" gorm:"uniqueIndex:idx_webdav_property"`
+ Lang string `json:"lang"`
+ InnerXML []byte `json:"inner_xml"`
+}
diff --git a/internal/op/fs.go b/internal/op/fs.go
index f82a3ca8f..90c1545bf 100644
--- a/internal/op/fs.go
+++ b/internal/op/fs.go
@@ -514,7 +514,7 @@ func Copy(ctx context.Context, storage driver.Driver, srcPath, dstDirPath string
srcPath = utils.FixAndCleanPath(srcPath)
dstDirPath = utils.FixAndCleanPath(dstDirPath)
if dstDirPath == stdpath.Dir(srcPath) {
- return errors.New("copy in place")
+ return errors.WithStack(errs.NotImplement)
}
srcRawObj, err := Get(ctx, storage, srcPath, true)
if err != nil {
diff --git a/server/webdav/file.go b/server/webdav/file.go
index ea6099735..4d2d5a25e 100644
--- a/server/webdav/file.go
+++ b/server/webdav/file.go
@@ -55,21 +55,43 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
if !common.CanWrite(user, srcMeta, srcDir) || !common.CanWrite(user, dstMeta, dstDir) {
return http.StatusForbidden, nil
}
+ if dstDirInfo, err := fs.Get(ctx, dstDir, &fs.GetArgs{}); err != nil {
+ if errs.IsObjectNotFound(err) {
+ return http.StatusConflict, err
+ }
+ return http.StatusMethodNotAllowed, err
+ } else if !dstDirInfo.IsDir() {
+ return http.StatusConflict, nil
+ }
+ dstExisted := false
+ if _, err = fs.Get(ctx, dst, &fs.GetArgs{}); err == nil {
+ dstExisted = true
+ if !overwrite {
+ return http.StatusPreconditionFailed, nil
+ }
+ if err = fs.Remove(ctx, dst); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ } else if !errs.IsObjectNotFound(err) {
+ return http.StatusInternalServerError, err
+ }
if srcDir == dstDir {
err = fs.Rename(ctx, src, dstName)
} else {
_, err = fs.Move(context.WithValue(ctx, conf.NoTaskKey, struct{}{}), src, dstDir)
- if err != nil {
- return http.StatusInternalServerError, err
- }
- if srcName != dstName {
+ if err == nil && srcName != dstName {
err = fs.Rename(ctx, path.Join(dstDir, srcName), dstName)
}
}
if err != nil {
return http.StatusInternalServerError, err
}
- // TODO if there are no files copy, should return 204
+ if err = moveDeadProps(src, dst); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ if dstExisted {
+ return http.StatusNoContent, nil
+ }
return http.StatusCreated, nil
}
@@ -80,6 +102,7 @@ func moveFiles(ctx context.Context, src, dst string, overwrite bool) (status int
func copyFiles(ctx context.Context, src, dst string, overwrite bool) (status int, err error) {
srcDir := path.Dir(src)
dstDir := path.Dir(dst)
+ dstName := path.Base(dst)
user := ctx.Value(conf.UserKey).(*model.User)
if !user.CanCopy() {
return http.StatusForbidden, nil
@@ -98,11 +121,34 @@ func copyFiles(ctx context.Context, src, dst string, overwrite bool) (status int
if !common.CanWrite(user, dstMeta, dstDir) {
return http.StatusForbidden, nil
}
- _, err = fs.Copy(context.WithValue(ctx, conf.NoTaskKey, struct{}{}), src, dstDir)
+ if dstDirInfo, err := fs.Get(ctx, dstDir, &fs.GetArgs{}); err != nil {
+ if errs.IsObjectNotFound(err) {
+ return http.StatusConflict, err
+ }
+ return http.StatusMethodNotAllowed, err
+ } else if !dstDirInfo.IsDir() {
+ return http.StatusConflict, nil
+ }
+ dstExisted := false
+ if _, err = fs.Get(ctx, dst, &fs.GetArgs{}); err == nil {
+ dstExisted = true
+ if !overwrite {
+ return http.StatusPreconditionFailed, nil
+ }
+ if err = fs.Remove(ctx, dst); err != nil {
+ return http.StatusInternalServerError, err
+ }
+ } else if !errs.IsObjectNotFound(err) {
+ return http.StatusInternalServerError, err
+ }
+
+ _, err = fs.CopyTo(context.WithValue(ctx, conf.NoTaskKey, struct{}{}), src, dstDir, dstName)
if err != nil {
return http.StatusInternalServerError, err
}
- // TODO if there are no files copy, should return 204
+ if dstExisted {
+ return http.StatusNoContent, nil
+ }
return http.StatusCreated, nil
}
diff --git a/server/webdav/lock.go b/server/webdav/lock.go
index 344ac5cea..b3d5ac5ee 100644
--- a/server/webdav/lock.go
+++ b/server/webdav/lock.go
@@ -109,6 +109,8 @@ type LockDetails struct {
// ZeroDepth is whether the lock has zero depth. If it does not have zero
// depth, it has infinite depth.
ZeroDepth bool
+ // Shared is whether the lock may coexist with other shared locks on Root.
+ Shared bool
}
// NewMemLS returns a new in-memory LockSystem.
@@ -184,15 +186,10 @@ func (m *memLS) Confirm(now time.Time, name0, name1 string, conditions ...Condit
}, nil
}
-// lookup returns the node n that locks the named resource, provided that n
-// matches at least one of the given conditions and that lock isn't held by
-// another party. Otherwise, it returns nil.
-//
-// n may be a parent of the named resource, if n is an infinite depth lock.
-func (m *memLS) lookup(name string, conditions ...Condition) (n *memLSNode) {
+func (m *memLS) lookup(name string, conditions ...Condition) *memLSNode {
// TODO: support Condition.Not and Condition.ETag.
for _, c := range conditions {
- n = m.byToken[c.Token]
+ n := m.byToken[c.Token]
if n == nil || n.held {
continue
}
@@ -235,6 +232,14 @@ func (m *memLS) Create(now time.Time, details LockDetails) (string, error) {
m.collectExpiredNodes(now)
details.Root = slashClean(details.Root)
+ if details.Shared {
+ if n := m.byName[details.Root]; n != nil && n.token != "" && n.details.Shared && n.details.ZeroDepth == details.ZeroDepth && !n.held {
+ token := m.nextToken()
+ n.sharedTokens[token] = struct{}{}
+ m.byToken[token] = n
+ return token, nil
+ }
+ }
if !m.canCreate(details.Root, details.ZeroDepth) {
return "", ErrLocked
}
@@ -242,6 +247,9 @@ func (m *memLS) Create(now time.Time, details LockDetails) (string, error) {
n.token = m.nextToken()
m.byToken[n.token] = n
n.details = details
+ if details.Shared {
+ n.sharedTokens = map[string]struct{}{n.token: {}}
+ }
if n.details.Duration >= 0 {
n.expiry = now.Add(n.details.Duration)
heap.Push(&m.byExpiry, n)
@@ -284,6 +292,13 @@ func (m *memLS) Unlock(now time.Time, token string) error {
if n.held {
return ErrLocked
}
+ if n.details.Shared {
+ delete(m.byToken, token)
+ delete(n.sharedTokens, token)
+ if len(n.sharedTokens) != 0 {
+ return nil
+ }
+ }
m.remove(n)
return nil
}
@@ -334,7 +349,13 @@ func (m *memLS) create(name string) (ret *memLSNode) {
}
func (m *memLS) remove(n *memLSNode) {
- delete(m.byToken, n.token)
+ if n.details.Shared {
+ for token := range n.sharedTokens {
+ delete(m.byToken, token)
+ }
+ } else {
+ delete(m.byToken, n.token)
+ }
n.token = ""
walkToRoot(n.details.Root, func(name0 string, first bool) bool {
x := m.byName[name0]
@@ -380,7 +401,8 @@ type memLSNode struct {
// if this node does not expire, or has expired.
byExpiryIndex int
// held is whether this node's lock is actively held by a Confirm call.
- held bool
+ held bool
+ sharedTokens map[string]struct{}
}
type byExpiry []*memLSNode
diff --git a/server/webdav/prop.go b/server/webdav/prop.go
index 5c8889341..886f120c7 100644
--- a/server/webdav/prop.go
+++ b/server/webdav/prop.go
@@ -16,9 +16,11 @@ import (
"time"
"github.com/OpenListTeam/OpenList/v4/internal/conf"
+ "github.com/OpenListTeam/OpenList/v4/internal/db"
"github.com/OpenListTeam/OpenList/v4/internal/model"
"github.com/OpenListTeam/OpenList/v4/pkg/utils"
"github.com/OpenListTeam/OpenList/v4/server/common"
+ "gorm.io/gorm"
)
// Proppatch describes a property update instruction as defined in RFC 4918.
@@ -170,79 +172,39 @@ var liveProps = map[xml.Name]struct {
// TODO(nigeltao) merge props and allprop?
// Props returns the status of the properties named pnames for resource name.
-//
-// Each Propstat has a unique status and each property name will only be part
-// of one Propstat element.
-func props(ctx context.Context, ls LockSystem, fi model.Obj, pnames []xml.Name) ([]Propstat, error) {
- //f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
- //if err != nil {
- // return nil, err
- //}
- //defer f.Close()
- //fi, err := f.Stat()
- //if err != nil {
- // return nil, err
- //}
+func props(ctx context.Context, ls LockSystem, name string, fi model.Obj, pnames []xml.Name) ([]Propstat, error) {
isDir := fi.IsDir()
-
- var deadProps map[xml.Name]Property
- // ??? what is this for?
- //if dph, ok := f.(DeadPropsHolder); ok {
- // deadProps, err = dph.DeadProps()
- // if err != nil {
- // return nil, err
- // }
- //}
-
+ deadProps, err := getDeadProps(name)
+ if err != nil {
+ return nil, err
+ }
pstatOK := Propstat{Status: http.StatusOK}
pstatNotFound := Propstat{Status: http.StatusNotFound}
for _, pn := range pnames {
- // If this file has dead properties, check if they contain pn.
if dp, ok := deadProps[pn]; ok {
pstatOK.Props = append(pstatOK.Props, dp)
continue
}
- // Otherwise, it must either be a live property or we don't know it.
if prop := liveProps[pn]; prop.findFn != nil && (prop.dir || !isDir) {
innerXML, err := prop.findFn(ctx, ls, fi.GetName(), fi)
if err != nil {
return nil, err
}
- pstatOK.Props = append(pstatOK.Props, Property{
- XMLName: pn,
- InnerXML: []byte(innerXML),
- })
+ pstatOK.Props = append(pstatOK.Props, Property{XMLName: pn, InnerXML: []byte(innerXML)})
} else {
- pstatNotFound.Props = append(pstatNotFound.Props, Property{
- XMLName: pn,
- })
+ pstatNotFound.Props = append(pstatNotFound.Props, Property{XMLName: pn})
}
}
return makePropstats(pstatOK, pstatNotFound), nil
}
// Propnames returns the property names defined for resource name.
-func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, error) {
- //f, err := fs.OpenFile(ctx, name, os.O_RDONLY, 0)
- //if err != nil {
- // return nil, err
- //}
- //defer f.Close()
- //fi, err := f.Stat()
- //if err != nil {
- // return nil, err
- //}
+func propnames(_ context.Context, _ LockSystem, name string, fi model.Obj) ([]xml.Name, error) {
isDir := fi.IsDir()
-
- var deadProps map[xml.Name]Property
- // ??? what is this for?
- //if dph, ok := f.(DeadPropsHolder); ok {
- // deadProps, err = dph.DeadProps()
- // if err != nil {
- // return nil, err
- // }
- //}
-
+ deadProps, err := getDeadProps(name)
+ if err != nil {
+ return nil, err
+ }
pnames := make([]xml.Name, 0, len(liveProps)+len(deadProps))
for pn, prop := range liveProps {
if prop.findFn != nil && (prop.dir || !isDir) {
@@ -255,20 +217,12 @@ func propnames(ctx context.Context, ls LockSystem, fi model.Obj) ([]xml.Name, er
return pnames, nil
}
-// Allprop returns the properties defined for resource name and the properties
-// named in include.
-//
-// Note that RFC 4918 defines 'allprop' to return the DAV: properties defined
-// within the RFC plus dead properties. Other live properties should only be
-// returned if they are named in 'include'.
-//
-// See http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
-func allprop(ctx context.Context, ls LockSystem, fi model.Obj, include []xml.Name) ([]Propstat, error) {
- pnames, err := propnames(ctx, ls, fi)
+// Allprop returns the properties defined for resource name and the properties named in include.
+func allprop(ctx context.Context, ls LockSystem, name string, fi model.Obj, include []xml.Name) ([]Propstat, error) {
+ pnames, err := propnames(ctx, ls, name, fi)
if err != nil {
return nil, err
}
- // Add names from include if they are not already covered in pnames.
nameset := make(map[xml.Name]bool)
for _, pn := range pnames {
nameset[pn] = true
@@ -278,11 +232,10 @@ func allprop(ctx context.Context, ls LockSystem, fi model.Obj, include []xml.Nam
pnames = append(pnames, pn)
}
}
- return props(ctx, ls, fi, pnames)
+ return props(ctx, ls, name, fi, pnames)
}
-// Patch patches the properties of resource name. The return values are
-// constrained in the same manner as DeadPropsHolder.Patch.
+// Patch patches the properties of resource name.
func patch(ctx context.Context, ls LockSystem, name string, patches []Proppatch) ([]Propstat, error) {
conflict := false
loop:
@@ -314,53 +267,78 @@ loop:
return makePropstats(pstatForbidden, pstatFailedDep), nil
}
- // ------------------------------------------------------------
- //f, err := fs.OpenFile(ctx, name, os.O_RDWR, 0)
- //if err != nil {
- // return nil, err
- //}
- //defer f.Close()
- //if dph, ok := f.(DeadPropsHolder); ok {
- // ret, err := dph.Patch(patches)
- // if err != nil {
- // return nil, err
- // }
- // // http://www.webdav.org/specs/rfc4918.html#ELEMENT_propstat says that
- // // "The contents of the prop XML element must only list the names of
- // // properties to which the result in the status element applies."
- // for _, pstat := range ret {
- // for i, p := range pstat.Props {
- // pstat.Props[i] = Property{XMLName: p.XMLName}
- // }
- // }
- // return ret, nil
- //}
- // ------------------------------------------------------------
-
- // The file doesn't implement the optional DeadPropsHolder interface, so
- // all patches are forbidden.
- pstat := Propstat{Status: http.StatusForbidden}
- for _, patch := range patches {
- for _, p := range patch.Props {
- pstat.Props = append(pstat.Props, Property{XMLName: p.XMLName})
+ database := db.GetDb()
+ if database == nil {
+ return nil, errors.New("webdav property database is not initialized")
+ }
+ pstat := Propstat{Status: http.StatusOK}
+ var err error
+ for attempt := 0; attempt < 40; attempt++ {
+ pstat.Props = nil
+ err = database.Transaction(func(tx *gorm.DB) error {
+ for _, patch := range patches {
+ for _, prop := range patch.Props {
+ if patch.Remove {
+ if err := tx.Where("path = ? AND namespace = ? AND name = ?", name, prop.XMLName.Space, prop.XMLName.Local).Delete(&model.WebDAVProperty{}).Error; err != nil {
+ return err
+ }
+ } else {
+ row := model.WebDAVProperty{
+ Path: name,
+ Namespace: prop.XMLName.Space,
+ Name: prop.XMLName.Local,
+ }
+ if err := tx.Where("path = ? AND namespace = ? AND name = ?", row.Path, row.Namespace, row.Name).
+ Assign(model.WebDAVProperty{Lang: prop.Lang, InnerXML: prop.InnerXML}).
+ FirstOrCreate(&row).Error; err != nil {
+ return err
+ }
+ }
+ pstat.Props = append(pstat.Props, Property{XMLName: prop.XMLName})
+ }
+ }
+ return nil
+ })
+ if err == nil || !strings.Contains(err.Error(), "database is locked") {
+ break
}
+ time.Sleep(time.Duration(attempt+1) * 50 * time.Millisecond)
+ }
+ if err != nil {
+ return nil, err
}
return []Propstat{pstat}, nil
}
+func getDeadProps(path string) (map[xml.Name]Property, error) {
+ database := db.GetDb()
+ if database == nil {
+ return nil, errors.New("webdav property database is not initialized")
+ }
+ var rows []model.WebDAVProperty
+ if err := database.Where("path = ?", path).Find(&rows).Error; err != nil {
+ return nil, err
+ }
+ props := make(map[xml.Name]Property, len(rows))
+ for _, row := range rows {
+ props[xml.Name{Space: row.Namespace, Local: row.Name}] = Property{
+ XMLName: xml.Name{Space: row.Namespace, Local: row.Name},
+ Lang: row.Lang,
+ InnerXML: row.InnerXML,
+ }
+ }
+ return props, nil
+}
+
func escapeXML(s string) string {
for i := 0; i < len(s); i++ {
- // As an optimization, if s contains only ASCII letters, digits or a
- // few special characters, the escaped value is s itself and we don't
- // need to allocate a buffer and convert between string and []byte.
switch c := s[i]; {
case c == ' ' || c == '_' ||
- ('+' <= c && c <= '9') || // Digits as well as + , - . and /
+ ('+' <= c && c <= '9') ||
('A' <= c && c <= 'Z') ||
('a' <= c && c <= 'z'):
continue
}
- // Otherwise, go through the full escaping process.
var buf bytes.Buffer
xml.EscapeText(&buf, []byte(s))
return buf.String()
@@ -368,6 +346,38 @@ func escapeXML(s string) string {
return s
}
+func moveDeadProps(src, dst string) error {
+ database := db.GetDb()
+ if database == nil {
+ return errors.New("webdav property database is not initialized")
+ }
+ escapedSrc := strings.ReplaceAll(strings.ReplaceAll(src, "%", "\\%"), "_", "\\_")
+ escapedDst := strings.ReplaceAll(strings.ReplaceAll(dst, "%", "\\%"), "_", "\\_")
+ return database.Transaction(func(tx *gorm.DB) error {
+ // Clear destination subtree for overwrite MOVE.
+ if err := tx.Where("path = ? OR path LIKE ? ESCAPE '\\'", dst, escapedDst+"/%").Delete(&model.WebDAVProperty{}).Error; err != nil {
+ return err
+ }
+ var rows []model.WebDAVProperty
+ if err := tx.Where("path = ? OR path LIKE ? ESCAPE '\\'", src, escapedSrc+"/%").Find(&rows).Error; err != nil {
+ return err
+ }
+ for _, row := range rows {
+ newPath := dst + strings.TrimPrefix(row.Path, src)
+ copy := row
+ copy.ID = 0
+ copy.Path = newPath
+ if err := tx.Create(©).Error; err != nil {
+ return err
+ }
+ if err := tx.Delete(&row).Error; err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
func findResourceType(ctx context.Context, ls LockSystem, name string, fi model.Obj) (string, error) {
if fi.IsDir() {
return ``, nil
diff --git a/server/webdav/webdav.go b/server/webdav/webdav.go
index 06d1431ac..91a365daa 100644
--- a/server/webdav/webdav.go
+++ b/server/webdav/webdav.go
@@ -82,10 +82,6 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
status, err = h.handleUnlock(brw, r)
case "PROPFIND":
status, err = h.handlePropfind(brw, r)
- // if there is a error for PROPFIND, we should be as an empty folder to the client
- if err != nil {
- status = http.StatusNotFound
- }
case "PROPPATCH":
status, err = h.handleProppatch(brw, r)
}
@@ -122,11 +118,6 @@ func (h *Handler) lock(now time.Time, root string) (token string, status int, er
func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func(), status int, err error) {
hdr := r.Header.Get("If")
if hdr == "" {
- // An empty If header means that the client hasn't previously created locks.
- // Even if this client doesn't care about locks, we still need to check that
- // the resources aren't locked by another client, so we create temporary
- // locks that would conflict with another client's locks. These temporary
- // locks are unlocked at the end of the HTTP request.
now, srcToken, dstToken := time.Now(), "", ""
if src != "" {
srcToken, status, err = h.lock(now, src)
@@ -143,7 +134,6 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func()
return nil, status, err
}
}
-
return func() {
if dstToken != "" {
h.LockSystem.Unlock(now, dstToken)
@@ -158,7 +148,7 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func()
if !ok {
return nil, http.StatusBadRequest, errInvalidIfHeader
}
- // ih is a disjunction (OR) of ifLists, so any ifList will do.
+ user, _ := r.Context().Value(conf.UserKey).(*model.User)
for _, l := range ih.lists {
lsrc := l.resourceTag
if lsrc == "" {
@@ -175,6 +165,12 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func()
if err != nil {
return nil, status, err
}
+ if user != nil {
+ lsrc, err = user.JoinPath(lsrc)
+ if err != nil {
+ return nil, http.StatusForbidden, err
+ }
+ }
}
release, err = h.LockSystem.Confirm(time.Now(), lsrc, dst, l.conditions...)
if err == ErrConfirmationFailed {
@@ -185,10 +181,6 @@ func (h *Handler) confirmLocks(r *http.Request, src, dst string) (release func()
}
return release, 0, nil
}
- // Section 10.4.1 says that "If this header is evaluated and all state lists
- // fail, then the request must fail with a 412 (Precondition Failed) status."
- // We follow the spec even though the cond_put_corrupt_token test case from
- // the litmus test warns on seeing a 412 instead of a 423 (Locked).
return nil, http.StatusPreconditionFailed, ErrLocked
}
@@ -212,9 +204,7 @@ func (h *Handler) handleOptions(w http.ResponseWriter, r *http.Request) (status
}
}
w.Header().Set("Allow", allow)
- // http://www.webdav.org/specs/rfc4918.html#dav.compliance.classes
w.Header().Set("DAV", "1, 2")
- // http://msdn.microsoft.com/en-au/library/cc250217.aspx
w.Header().Set("MS-Author-Via", "DAV")
return 0, nil
}
@@ -295,12 +285,6 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i
if err != nil {
return status, err
}
- release, status, err := h.confirmLocks(r, reqPath, "")
- if err != nil {
- return status, err
- }
- defer release()
-
ctx := r.Context()
user := ctx.Value(conf.UserKey).(*model.User)
if !user.CanRemove() {
@@ -310,6 +294,11 @@ func (h *Handler) handleDelete(w http.ResponseWriter, r *http.Request) (status i
if err != nil {
return http.StatusForbidden, err
}
+ release, status, err := h.confirmLocks(r, reqPath, "")
+ if err != nil {
+ return status, err
+ }
+ defer release()
// TODO: return MultiStatus where appropriate.
// "godoc os RemoveAll" says that "If the path does not exist, RemoveAll
@@ -350,11 +339,6 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if reqPath == "" {
return http.StatusMethodNotAllowed, nil
}
- release, status, err := h.confirmLocks(r, reqPath, "")
- if err != nil {
- return status, err
- }
- defer release()
// TODO(rost): Support the If-Match, If-None-Match headers? See bradfitz'
// comments in http.checkEtag.
ctx := r.Context()
@@ -363,6 +347,11 @@ func (h *Handler) handlePut(w http.ResponseWriter, r *http.Request) (status int,
if err != nil {
return http.StatusForbidden, err
}
+ release, status, err := h.confirmLocks(r, reqPath, "")
+ if err != nil {
+ return status, err
+ }
+ defer release()
size := r.ContentLength
if size < 0 {
sizeStr := r.Header.Get("X-File-Size")
@@ -428,18 +417,17 @@ func (h *Handler) handleMkcol(w http.ResponseWriter, r *http.Request) (status in
if err != nil {
return status, err
}
- release, status, err := h.confirmLocks(r, reqPath, "")
- if err != nil {
- return status, err
- }
- defer release()
-
ctx := r.Context()
user := ctx.Value(conf.UserKey).(*model.User)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return http.StatusForbidden, err
}
+ release, status, err := h.confirmLocks(r, reqPath, "")
+ if err != nil {
+ return status, err
+ }
+ defer release()
if r.ContentLength > 0 {
return http.StatusUnsupportedMediaType, nil
@@ -627,6 +615,7 @@ func (h *Handler) handleLock(w http.ResponseWriter, r *http.Request) (retStatus
Duration: duration,
OwnerXML: li.Owner.InnerXML,
ZeroDepth: depth == 0,
+ Shared: li.Shared != nil,
}
token, err = h.LockSystem.Create(now, ld)
if err != nil {
@@ -758,7 +747,7 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
}
var pstats []Propstat
if pf.Propname != nil {
- pnames, err := propnames(ctx, h.LockSystem, info)
+ pnames, err := propnames(ctx, h.LockSystem, reqPath, info)
if err != nil {
return err
}
@@ -768,9 +757,9 @@ func (h *Handler) handlePropfind(w http.ResponseWriter, r *http.Request) (status
}
pstats = append(pstats, pstat)
} else if pf.Allprop != nil {
- pstats, err = allprop(ctx, h.LockSystem, info, pf.Prop)
+ pstats, err = allprop(ctx, h.LockSystem, reqPath, info, pf.Prop)
} else {
- pstats, err = props(ctx, h.LockSystem, info, pf.Prop)
+ pstats, err = props(ctx, h.LockSystem, reqPath, info, pf.Prop)
}
if err != nil {
return err
@@ -798,18 +787,17 @@ func (h *Handler) handleProppatch(w http.ResponseWriter, r *http.Request) (statu
if err != nil {
return status, err
}
- release, status, err := h.confirmLocks(r, reqPath, "")
- if err != nil {
- return status, err
- }
- defer release()
-
ctx := r.Context()
user := ctx.Value(conf.UserKey).(*model.User)
reqPath, err = user.JoinPath(reqPath)
if err != nil {
return http.StatusForbidden, err
}
+ release, status, err := h.confirmLocks(r, reqPath, "")
+ if err != nil {
+ return status, err
+ }
+ defer release()
meta, err := op.GetNearestMeta(reqPath)
if err != nil && !errors.Is(errors.Cause(err), errs.MetaNotFound) {
return http.StatusInternalServerError, err
diff --git a/server/webdav/xml.go b/server/webdav/xml.go
index c9ec61dff..d87eb863c 100644
--- a/server/webdav/xml.go
+++ b/server/webdav/xml.go
@@ -62,9 +62,7 @@ func readLockInfo(r io.Reader) (li lockInfo, status int, err error) {
}
return lockInfo{}, http.StatusBadRequest, err
}
- // We only support exclusive (non-shared) write locks. In practice, these are
- // the only types of locks that seem to matter.
- if li.Exclusive == nil || li.Shared != nil || li.Write == nil {
+ if (li.Exclusive == nil) == (li.Shared == nil) || li.Write == nil {
return lockInfo{}, http.StatusNotImplemented, errUnsupportedLockInfo
}
return li, 0, nil
@@ -86,18 +84,22 @@ func writeLockInfo(w io.Writer, token string, ld LockDetails) (int, error) {
if ld.ZeroDepth {
depth = "0"
}
+ scope := "exclusive"
+ if ld.Shared {
+ scope = "shared"
+ }
timeout := ld.Duration / time.Second
return fmt.Fprintf(w, "\n"+
"\n"+
- " \n"+
- " \n"+
- " %s\n"+
- " %s\n"+
- " Second-%d\n"+
- " %s\n"+
- " %s\n"+
+ "\t\n"+
+ "\t\n"+
+ "\t%s\n"+
+ "\t%s\n"+
+ "\tSecond-%d\n"+
+ "\t%s\n"+
+ "\t%s\n"+
"",
- depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root),
+ scope, depth, ld.OwnerXML, timeout, escape(token), escape(ld.Root),
)
}
@@ -176,19 +178,31 @@ type propfind struct {
}
func readPropfind(r io.Reader) (pf propfind, status int, err error) {
- c := countingReader{r: r}
- if err = ixml.NewDecoder(&c).Decode(&pf); err != nil {
+ // 64KB is more than enough for a well-formed PROPFIND body.
+ const maxBody = 64 << 10
+ body, err := io.ReadAll(io.LimitReader(r, maxBody))
+ if err != nil {
+ return propfind{}, http.StatusBadRequest, err
+ }
+ // If the limit was reached, the body was too large.
+ if len(body) >= maxBody {
+ // Drain any remaining bytes so the connection stays usable.
+ _, _ = io.Copy(io.Discard, r)
+ return propfind{}, http.StatusRequestEntityTooLarge, nil
+ }
+ if len(body) == 0 {
+ // An empty body means to propfind allprop.
+ return propfind{Allprop: new(struct{})}, 0, nil
+ }
+ if hasEmptyNamespacePrefix(body) {
+ return propfind{}, http.StatusBadRequest, errInvalidPropfind
+ }
+ if err = ixml.NewDecoder(bytes.NewReader(body)).Decode(&pf); err != nil {
if err == io.EOF {
- if c.n == 0 {
- // An empty body means to propfind allprop.
- // http://www.webdav.org/specs/rfc4918.html#METHOD_PROPFIND
- return propfind{Allprop: new(struct{})}, 0, nil
- }
err = errInvalidPropfind
}
return propfind{}, http.StatusBadRequest, err
}
-
if pf.Allprop == nil && pf.Include != nil {
return propfind{}, http.StatusBadRequest, errInvalidPropfind
}
@@ -204,6 +218,33 @@ func readPropfind(r io.Reader) (pf propfind, status int, err error) {
return pf, 0, nil
}
+func hasEmptyNamespacePrefix(body []byte) bool {
+ for offset := 0; ; {
+ i := bytes.Index(body[offset:], []byte("xmlns:"))
+ if i < 0 {
+ return false
+ }
+ i += offset + len("xmlns:")
+ j := i
+ for j < len(body) && body[j] != '=' && body[j] != '>' && body[j] != '/' && body[j] != ' ' && body[j] != '\t' && body[j] != '\n' && body[j] != '\r' {
+ j++
+ }
+ for j < len(body) && (body[j] == ' ' || body[j] == '\t' || body[j] == '\n' || body[j] == '\r') {
+ j++
+ }
+ if j < len(body) && body[j] == '=' {
+ j++
+ for j < len(body) && (body[j] == ' ' || body[j] == '\t' || body[j] == '\n' || body[j] == '\r') {
+ j++
+ }
+ if j+1 < len(body) && (body[j] == '\'' || body[j] == '"') && body[j+1] == body[j] {
+ return true
+ }
+ }
+ offset = i
+ }
+}
+
// Property represents a single DAV resource property as defined in RFC 4918.
// See http://www.webdav.org/specs/rfc4918.html#data.model.for.resource.properties
type Property struct {