Commit 8b2d0ae1 authored by Nigel Tao's avatar Nigel Tao

webdav: add a FileSystem.Rename method.

It will be used by the WebDAV MOVE method.

Change-Id: Iacbcf58af4da0d169271661324537825d4ff887c
Reviewed-on: https://go-review.googlesource.com/2952Reviewed-by: 's avatarDave Cheney <dave@cheney.net>
parent 5a08939a
...@@ -15,6 +15,15 @@ import ( ...@@ -15,6 +15,15 @@ import (
"time" "time"
) )
// slashClean is equivalent to but slightly more efficient than
// path.Clean("/" + name).
func slashClean(name string) string {
if name == "" || name[0] != '/' {
name = "/" + name
}
return path.Clean(name)
}
// A FileSystem implements access to a collection of named files. The elements // A FileSystem implements access to a collection of named files. The elements
// in a file path are separated by slash ('/', U+002F) characters, regardless // in a file path are separated by slash ('/', U+002F) characters, regardless
// of host operating system convention. // of host operating system convention.
...@@ -25,6 +34,7 @@ type FileSystem interface { ...@@ -25,6 +34,7 @@ type FileSystem interface {
Mkdir(name string, perm os.FileMode) error Mkdir(name string, perm os.FileMode) error
OpenFile(name string, flag int, perm os.FileMode) (File, error) OpenFile(name string, flag int, perm os.FileMode) (File, error)
RemoveAll(name string) error RemoveAll(name string) error
Rename(oldName, newName string) error
Stat(name string) (os.FileInfo, error) Stat(name string) (os.FileInfo, error)
} }
...@@ -55,7 +65,7 @@ func (d Dir) resolve(name string) string { ...@@ -55,7 +65,7 @@ func (d Dir) resolve(name string) string {
if dir == "" { if dir == "" {
dir = "." dir = "."
} }
return filepath.Join(dir, filepath.FromSlash(path.Clean("/"+name))) return filepath.Join(dir, filepath.FromSlash(slashClean(name)))
} }
func (d Dir) Mkdir(name string, perm os.FileMode) error { func (d Dir) Mkdir(name string, perm os.FileMode) error {
...@@ -87,6 +97,20 @@ func (d Dir) RemoveAll(name string) error { ...@@ -87,6 +97,20 @@ func (d Dir) RemoveAll(name string) error {
return os.RemoveAll(name) return os.RemoveAll(name)
} }
func (d Dir) Rename(oldName, newName string) error {
if oldName = d.resolve(oldName); oldName == "" {
return os.ErrNotExist
}
if newName = d.resolve(newName); newName == "" {
return os.ErrNotExist
}
if root := filepath.Clean(string(d)); root == oldName || root == newName {
// Prohibit renaming from or to the virtual root directory.
return os.ErrInvalid
}
return os.Rename(oldName, newName)
}
func (d Dir) Stat(name string) (os.FileInfo, error) { func (d Dir) Stat(name string) (os.FileInfo, error) {
if name = d.resolve(name); name == "" { if name = d.resolve(name); name == "" {
return nil, os.ErrNotExist return nil, os.ErrNotExist
...@@ -118,12 +142,11 @@ type memFS struct { ...@@ -118,12 +142,11 @@ type memFS struct {
root memFSNode root memFSNode
} }
// TODO: clean up and rationalize the walk/find code.
// walk walks the directory tree for the fullname, calling f at each step. If f // walk walks the directory tree for the fullname, calling f at each step. If f
// returns an error, the walk will be aborted and return that same error. // returns an error, the walk will be aborted and return that same error.
// //
// Each walk is atomic: fs's mutex is held for the entire operation, including
// all calls to f.
//
// dir is the directory at that step, frag is the name fragment, and final is // dir is the directory at that step, frag is the name fragment, and final is
// whether it is the final step. For example, walking "/foo/bar/x" will result // whether it is the final step. For example, walking "/foo/bar/x" will result
// in 3 calls to f: // in 3 calls to f:
...@@ -133,11 +156,8 @@ type memFS struct { ...@@ -133,11 +156,8 @@ type memFS struct {
// The frag argument will be empty only if dir is the root node and the walk // The frag argument will be empty only if dir is the root node and the walk
// ends at that root node. // ends at that root node.
func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, final bool) error) error { func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, final bool) error) error {
fs.mu.Lock()
defer fs.mu.Unlock()
original := fullname original := fullname
fullname = path.Clean("/" + fullname) fullname = slashClean(fullname)
// Strip any leading "/"s to make fullname a relative path, as the walk // Strip any leading "/"s to make fullname a relative path, as the walk
// starts at fs.root. // starts at fs.root.
...@@ -186,12 +206,39 @@ func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, f ...@@ -186,12 +206,39 @@ func (fs *memFS) walk(op, fullname string, f func(dir *memFSNode, frag string, f
return nil return nil
} }
func (fs *memFS) Mkdir(name string, perm os.FileMode) error { // find returns the parent of the named node and the relative name fragment
return fs.walk("mkdir", name, func(dir *memFSNode, frag string, final bool) error { // from the parent to the child. For example, if finding "/foo/bar/baz" then
// parent will be the node for "/foo/bar" and frag will be "baz".
//
// If the fullname names the root node, then parent, frag and err will be zero.
//
// find returns an error if the parent does not already exist or the parent
// isn't a directory, but it will not return an error per se if the child does
// not already exist. The error returned is either nil or an *os.PathError
// whose Op is op.
func (fs *memFS) find(op, fullname string) (parent *memFSNode, frag string, err error) {
err = fs.walk(op, fullname, func(parent0 *memFSNode, frag0 string, final bool) error {
if !final { if !final {
return nil return nil
} }
if frag == "" { if frag0 != "" {
parent, frag = parent0, frag0
}
return nil
})
return parent, frag, err
}
func (fs *memFS) Mkdir(name string, perm os.FileMode) error {
fs.mu.Lock()
defer fs.mu.Unlock()
dir, frag, err := fs.find("mkdir", name)
if err != nil {
return err
}
if dir == nil {
// We can't create the root.
return os.ErrInvalid return os.ErrInvalid
} }
if _, ok := dir.children[frag]; ok { if _, ok := dir.children[frag]; ok {
...@@ -204,30 +251,33 @@ func (fs *memFS) Mkdir(name string, perm os.FileMode) error { ...@@ -204,30 +251,33 @@ func (fs *memFS) Mkdir(name string, perm os.FileMode) error {
modTime: time.Now(), modTime: time.Now(),
} }
return nil return nil
})
} }
func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) {
var ret *memFile fs.mu.Lock()
err := fs.walk("open", name, func(dir *memFSNode, frag string, final bool) error { defer fs.mu.Unlock()
if !final {
return nil dir, frag, err := fs.find("open", name)
if err != nil {
return nil, err
} }
var n *memFSNode var n *memFSNode
if frag == "" { if dir == nil {
// We're opening the root.
if flag&(os.O_WRONLY|os.O_RDWR) != 0 { if flag&(os.O_WRONLY|os.O_RDWR) != 0 {
return os.ErrPermission return nil, os.ErrPermission
} }
n = &fs.root n = &fs.root
} else { } else {
n = dir.children[frag] n = dir.children[frag]
if flag&(os.O_SYNC|os.O_APPEND) != 0 { if flag&(os.O_SYNC|os.O_APPEND) != 0 {
return os.ErrInvalid // memFile doesn't support these flags yet.
return nil, os.ErrInvalid
} }
if flag&os.O_CREATE != 0 { if flag&os.O_CREATE != 0 {
if flag&os.O_EXCL != 0 && n != nil { if flag&os.O_EXCL != 0 && n != nil {
return os.ErrExist return nil, os.ErrExist
} }
if n == nil { if n == nil {
n = &memFSNode{ n = &memFSNode{
...@@ -238,7 +288,7 @@ func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) ...@@ -238,7 +288,7 @@ func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error)
} }
} }
if n == nil { if n == nil {
return os.ErrNotExist return nil, os.ErrNotExist
} }
if flag&(os.O_WRONLY|os.O_RDWR) != 0 && flag&os.O_TRUNC != 0 { if flag&(os.O_WRONLY|os.O_RDWR) != 0 && flag&os.O_TRUNC != 0 {
n.mu.Lock() n.mu.Lock()
...@@ -251,51 +301,98 @@ func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) ...@@ -251,51 +301,98 @@ func (fs *memFS) OpenFile(name string, flag int, perm os.FileMode) (File, error)
for _, c := range n.children { for _, c := range n.children {
children = append(children, c) children = append(children, c)
} }
ret = &memFile{ return &memFile{
n: n, n: n,
children: children, childrenSnapshot: children,
} }, nil
return nil
})
if err != nil {
return nil, err
}
return ret, nil
} }
func (fs *memFS) RemoveAll(name string) error { func (fs *memFS) RemoveAll(name string) error {
return fs.walk("remove", name, func(dir *memFSNode, frag string, final bool) error { fs.mu.Lock()
if !final { defer fs.mu.Unlock()
return nil
dir, frag, err := fs.find("remove", name)
if err != nil {
return err
} }
if frag == "" { if dir == nil {
// We can't remove the root.
return os.ErrInvalid return os.ErrInvalid
} }
delete(dir.children, frag) delete(dir.children, frag)
return nil return nil
})
} }
func (fs *memFS) Stat(name string) (os.FileInfo, error) { func (fs *memFS) Rename(oldName, newName string) error {
var n *memFSNode fs.mu.Lock()
err := fs.walk("stat", name, func(dir *memFSNode, frag string, final bool) error { defer fs.mu.Unlock()
if !final {
oldName = slashClean(oldName)
newName = slashClean(newName)
if oldName == newName {
return nil return nil
} }
if frag == "" { if strings.HasPrefix(newName, oldName+"/") {
n = &fs.root // We can't rename oldName to be a sub-directory of itself.
return nil return os.ErrInvalid
} }
n = dir.children[frag]
if n == nil { oDir, oFrag, err := fs.find("rename", oldName)
if err != nil {
return err
}
if oDir == nil {
// We can't rename from the root.
return os.ErrInvalid
}
nDir, nFrag, err := fs.find("rename", newName)
if err != nil {
return err
}
if nDir == nil {
// We can't rename to the root.
return os.ErrInvalid
}
oNode, ok := oDir.children[oFrag]
if !ok {
return os.ErrNotExist return os.ErrNotExist
} }
if oNode.IsDir() {
if nNode, ok := nDir.children[nFrag]; ok {
nNode.mu.Lock()
isDir := nNode.mode.IsDir()
nNode.mu.Unlock()
if !isDir {
return errNotADirectory
}
if len(nNode.children) != 0 {
return errDirectoryNotEmpty
}
}
}
delete(oDir.children, oFrag)
nDir.children[nFrag] = oNode
return nil return nil
}) }
func (fs *memFS) Stat(name string) (os.FileInfo, error) {
fs.mu.Lock()
defer fs.mu.Unlock()
dir, frag, err := fs.find("stat", name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if dir == nil {
// We're stat'ting the root.
return &fs.root, nil
}
if n, ok := dir.children[frag]; ok {
return n, nil return n, nil
}
return nil, os.ErrNotExist
} }
// A memFSNode represents a single entry in the in-memory filesystem and also // A memFSNode represents a single entry in the in-memory filesystem and also
...@@ -303,10 +400,12 @@ func (fs *memFS) Stat(name string) (os.FileInfo, error) { ...@@ -303,10 +400,12 @@ func (fs *memFS) Stat(name string) (os.FileInfo, error) {
type memFSNode struct { type memFSNode struct {
name string name string
// children is protected by memFS.mu.
children map[string]*memFSNode
mu sync.Mutex mu sync.Mutex
modTime time.Time modTime time.Time
mode os.FileMode mode os.FileMode
children map[string]*memFSNode
data []byte data []byte
} }
...@@ -345,8 +444,9 @@ func (n *memFSNode) Sys() interface{} { ...@@ -345,8 +444,9 @@ func (n *memFSNode) Sys() interface{} {
// that node's children. // that node's children.
type memFile struct { type memFile struct {
n *memFSNode n *memFSNode
children []os.FileInfo // childrenSnapshot is a snapshot of n.children.
// Changes to pos are guarded by n.mu. childrenSnapshot []os.FileInfo
// pos is protected by n.mu.
pos int pos int
} }
...@@ -375,7 +475,7 @@ func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { ...@@ -375,7 +475,7 @@ func (f *memFile) Readdir(count int) ([]os.FileInfo, error) {
return nil, os.ErrInvalid return nil, os.ErrInvalid
} }
old := f.pos old := f.pos
if old >= len(f.children) { if old >= len(f.childrenSnapshot) {
// The os.File Readdir docs say that at the end of a directory, // The os.File Readdir docs say that at the end of a directory,
// the error is io.EOF if count > 0 and nil if count <= 0. // the error is io.EOF if count > 0 and nil if count <= 0.
if count > 0 { if count > 0 {
...@@ -385,14 +485,14 @@ func (f *memFile) Readdir(count int) ([]os.FileInfo, error) { ...@@ -385,14 +485,14 @@ func (f *memFile) Readdir(count int) ([]os.FileInfo, error) {
} }
if count > 0 { if count > 0 {
f.pos += count f.pos += count
if f.pos > len(f.children) { if f.pos > len(f.childrenSnapshot) {
f.pos = len(f.children) f.pos = len(f.childrenSnapshot)
} }
} else { } else {
f.pos = len(f.children) f.pos = len(f.childrenSnapshot)
old = 0 old = 0
} }
return f.children[old:f.pos], nil return f.childrenSnapshot[old:f.pos], nil
} }
func (f *memFile) Seek(offset int64, whence int) (int64, error) { func (f *memFile) Seek(offset int64, whence int) (int64, error) {
......
...@@ -9,12 +9,37 @@ import ( ...@@ -9,12 +9,37 @@ import (
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
"path"
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"testing" "testing"
) )
func TestSlashClean(t *testing.T) {
testCases := []string{
"",
".",
"/",
"/./",
"//",
"//.",
"//a",
"/a",
"/a/b/c",
"/a//b/./../c/d/",
"a",
"a/b/c",
}
for _, tc := range testCases {
got := slashClean(tc)
want := path.Clean("/" + tc)
if got != want {
t.Errorf("tc=%q: got %q, want %q", tc, got, want)
}
}
}
func TestDirResolve(t *testing.T) { func TestDirResolve(t *testing.T) {
testCases := []struct { testCases := []struct {
dir, name, want string dir, name, want string
...@@ -260,6 +285,41 @@ func testFS(t *testing.T, fs FileSystem) { ...@@ -260,6 +285,41 @@ func testFS(t *testing.T, fs FileSystem) {
" stat /c want errNotExist", " stat /c want errNotExist",
" stat /d want dir", " stat /d want dir",
" stat /d/m want dir", " stat /d/m want dir",
"rename /b /b want ok",
" stat /b want 2",
" stat /c want errNotExist",
"rename /b /c want ok",
" stat /b want errNotExist",
" stat /c want 2",
" stat /d/m want dir",
" stat /d/n want errNotExist",
"rename /d/m /d/n want ok",
"create /d/n/q QQQQ want ok",
" stat /d/m want errNotExist",
" stat /d/n want dir",
" stat /d/n/q want 4",
"rename /d /d/n/x want err",
"rename /c /d/n/q want ok",
" stat /c want errNotExist",
" stat /d/n/q want 2",
"create /d/n/r RRRRR want ok",
"mk-dir /u want ok",
"mk-dir /u/v want ok",
"rename /d/n /u want err",
"create /t TTTTTT want ok",
"rename /d/n /t want err",
"rm-all /t want ok",
"rename /d/n /t want ok",
" stat /d want dir",
" stat /d/n want errNotExist",
" stat /d/n/r want errNotExist",
" stat /t want dir",
" stat /t/q want 2",
" stat /t/r want 5",
"rename /t / want err",
"rename /t /u/v want ok",
" stat /u/v/r want 5",
"rename / /x want err",
} }
for i, tc := range testCases { for i, tc := range testCases {
...@@ -292,9 +352,13 @@ func testFS(t *testing.T, fs FileSystem) { ...@@ -292,9 +352,13 @@ func testFS(t *testing.T, fs FileSystem) {
} }
} }
case "mk-dir", "rm-all", "stat": case "mk-dir", "rename", "rm-all", "stat":
nParts := 3
if op == "rename" {
nParts = 4
}
parts := strings.Split(arg, " ") parts := strings.Split(arg, " ")
if len(parts) != 3 { if len(parts) != nParts {
t.Fatalf("test case #%d %q: invalid %s", i, tc, op) t.Fatalf("test case #%d %q: invalid %s", i, tc, op)
} }
...@@ -302,6 +366,8 @@ func testFS(t *testing.T, fs FileSystem) { ...@@ -302,6 +366,8 @@ func testFS(t *testing.T, fs FileSystem) {
switch op { switch op {
case "mk-dir": case "mk-dir":
opErr = fs.Mkdir(parts[0], 0777) opErr = fs.Mkdir(parts[0], 0777)
case "rename":
opErr = fs.Rename(parts[0], parts[1])
case "rm-all": case "rm-all":
opErr = fs.RemoveAll(parts[0]) opErr = fs.RemoveAll(parts[0])
case "stat": case "stat":
...@@ -318,10 +384,10 @@ func testFS(t *testing.T, fs FileSystem) { ...@@ -318,10 +384,10 @@ func testFS(t *testing.T, fs FileSystem) {
got = errStr(opErr) got = errStr(opErr)
} }
if parts[1] != "want" { if parts[len(parts)-2] != "want" {
t.Fatalf("test case #%d %q: invalid %s", i, tc, op) t.Fatalf("test case #%d %q: invalid %s", i, tc, op)
} }
if want := parts[2]; got != want { if want := parts[len(parts)-1]; got != want {
t.Fatalf("test case #%d %q: got %q (%v), want %q", i, tc, got, opErr, want) t.Fatalf("test case #%d %q: got %q (%v), want %q", i, tc, got, opErr, want)
} }
} }
......
...@@ -6,7 +6,6 @@ package webdav ...@@ -6,7 +6,6 @@ package webdav
import ( import (
"errors" "errors"
"path"
"strconv" "strconv"
"strings" "strings"
"sync" "sync"
...@@ -145,7 +144,7 @@ func (m *memLS) Confirm(now time.Time, name string, conditions ...Condition) (Re ...@@ -145,7 +144,7 @@ func (m *memLS) Confirm(now time.Time, name string, conditions ...Condition) (Re
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.collectExpiredNodes(now) m.collectExpiredNodes(now)
name = path.Clean("/" + name) name = slashClean(name)
// TODO: touch n.held. // TODO: touch n.held.
panic("TODO") panic("TODO")
...@@ -155,7 +154,7 @@ func (m *memLS) Create(now time.Time, details LockDetails) (string, error) { ...@@ -155,7 +154,7 @@ func (m *memLS) Create(now time.Time, details LockDetails) (string, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
m.collectExpiredNodes(now) m.collectExpiredNodes(now)
name := path.Clean("/" + details.Root) name := slashClean(details.Root)
if !m.canCreate(name, details.ZeroDepth) { if !m.canCreate(name, details.ZeroDepth) {
return "", ErrLocked return "", ErrLocked
......
...@@ -354,11 +354,13 @@ func StatusText(code int) string { ...@@ -354,11 +354,13 @@ func StatusText(code int) string {
} }
var ( var (
errDirectoryNotEmpty = errors.New("webdav: directory not empty")
errInvalidDepth = errors.New("webdav: invalid depth") errInvalidDepth = errors.New("webdav: invalid depth")
errInvalidIfHeader = errors.New("webdav: invalid If header") errInvalidIfHeader = errors.New("webdav: invalid If header")
errInvalidLockInfo = errors.New("webdav: invalid lock info") errInvalidLockInfo = errors.New("webdav: invalid lock info")
errInvalidLockToken = errors.New("webdav: invalid lock token") errInvalidLockToken = errors.New("webdav: invalid lock token")
errNoFileSystem = errors.New("webdav: no file system") errNoFileSystem = errors.New("webdav: no file system")
errNoLockSystem = errors.New("webdav: no lock system") errNoLockSystem = errors.New("webdav: no lock system")
errNotADirectory = errors.New("webdav: not a directory")
errUnsupportedLockInfo = errors.New("webdav: unsupported lock info") errUnsupportedLockInfo = errors.New("webdav: unsupported lock info")
) )
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment