diff --git a/dummy.go b/dummy.go index a0359cd..c5fbf72 100644 --- a/dummy.go +++ b/dummy.go @@ -41,6 +41,11 @@ func (fs DummyFS) Mkdir(name string, perm os.FileMode) error { return fs.err } +// Symlink returns dummy error +func (fs DummyFS) Symlink(oldname, newname string) error { + return fs.err +} + // Stat returns dummy error func (fs DummyFS) Stat(name string) (os.FileInfo, error) { return nil, fs.err diff --git a/filesystem.go b/filesystem.go index 7271206..2ea54d0 100644 --- a/filesystem.go +++ b/filesystem.go @@ -22,7 +22,7 @@ type Filesystem interface { // RemoveAll(path string) error Rename(oldpath, newpath string) error Mkdir(name string, perm os.FileMode) error - // Symlink(oldname, newname string) error + Symlink(oldname, newname string) error // TempDir() string // Chmod(name string, mode FileMode) error // Chown(name string, uid, gid int) error diff --git a/memfs/memfs.go b/memfs/memfs.go index 6be44bd..c63aca9 100644 --- a/memfs/memfs.go +++ b/memfs/memfs.go @@ -31,6 +31,8 @@ type MemFS struct { lock *sync.RWMutex } +var _ vfs.Filesystem = &MemFS{} + // Create a new MemFS filesystem which entirely resides in memory func Create() *MemFS { root := &fileInfo{ @@ -130,6 +132,18 @@ func (fs *MemFS) Mkdir(name string, perm os.FileMode) error { return nil } +func (fs *MemFS) Symlink(oldname, newname string) error { + file, err := fs.OpenFile( + newname, + os.O_CREATE|os.O_WRONLY|os.O_TRUNC, + 0777|os.ModeSymlink) + if err != nil { + return err + } + file.Write([]byte(oldname)) + return nil +} + // byName implements sort.Interface type byName []os.FileInfo @@ -165,43 +179,50 @@ func (fs *MemFS) ReadDir(path string) ([]os.FileInfo, error) { } func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err error) { - path = filepath.Clean(path) - segments := vfs.SplitPath(path, PathSeparator) + return fs.relativeFileInfo(fs.wd, path) +} + +func (fs *MemFS) relativeFileInfo(wd *fileInfo, path string) (parent *fileInfo, node *fileInfo, err error) { + parent, segments := fs.dirSegments(wd, path) // Shortcut for working directory and root - if len(segments) == 1 { - if segments[0] == "" { - return nil, fs.root, nil - } else if segments[0] == "." { - return fs.wd.parent, fs.wd, nil - } + if len(segments) == 0 { + return parent.parent, parent, nil } - // Determine root to traverse - parent = fs.root - if segments[0] == "." { - parent = fs.wd - } - segments = segments[1:] + for _, seg := range segments[:len(segments)-1] { - // Further directories - if len(segments) > 1 { - for _, seg := range segments[:len(segments)-1] { - - if parent.childs == nil { - return nil, nil, os.ErrNotExist + if parent.childs == nil { + return nil, nil, os.ErrNotExist + } + entry, ok := parent.childs[seg] + if !ok { + return nil, nil, os.ErrNotExist + } + if entry.dir { + parent = entry + } else if entry.mode&os.ModeSymlink != 0 { + // Look up interior symlink + _, parent, err = fs.relativeFileInfo(parent, string(*entry.buf)) + if err != nil { + return nil, nil, err } - if entry, ok := parent.childs[seg]; ok && entry.dir { - parent = entry - } else { - return nil, nil, os.ErrNotExist + // Symlink was not to a directory + if parent == nil { + return nil, nil, vfs.ErrNotDirectory } + + } else { + return nil, nil, os.ErrNotExist } } lastSeg := segments[len(segments)-1] if parent.childs != nil { if node, ok := parent.childs[lastSeg]; ok { + if node.mode&os.ModeSymlink != 0 { + return fs.relativeFileInfo(parent, string(*node.buf)) + } return parent, node, nil } } else { @@ -211,6 +232,19 @@ func (fs *MemFS) fileInfo(path string) (parent *fileInfo, node *fileInfo, err er return parent, nil, nil } +func (fs *MemFS) dirSegments(wd *fileInfo, path string) (parent *fileInfo, segments []string) { + path = filepath.Clean(path) + segments = vfs.SplitPath(path, PathSeparator) + + // Determine root to traverse + parent = fs.root + if segments[0] == "." { + parent = wd + } + segments = segments[1:] + return parent, segments +} + func hasFlag(flag int, flags int) bool { return flags&flag == flag } diff --git a/memfs/memfs_test.go b/memfs/memfs_test.go index d2f06d4..fa1b4bc 100644 --- a/memfs/memfs_test.go +++ b/memfs/memfs_test.go @@ -3,6 +3,7 @@ package memfs import ( "io/ioutil" "os" + "strings" "testing" "time" @@ -126,6 +127,107 @@ func TestMkdirTree(t *testing.T) { //TODO: Subdir of file } +func TestSymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/tmp", 0755); err != nil { + t.Fatal("Unable to create /tmp:", err) + } + if err := vfs.WriteFile(fs, "/tmp/teacup", []byte("i am a teacup"), 0644); err != nil { + t.Fatal("Unable to fill teacup:", err) + } + err := fs.Symlink("/tmp/teacup", "/tmp/cup") + if err != nil { + t.Fatal("Symlink failed:", err) + } + fluid, err := vfs.ReadFile(fs, "/tmp/cup") + if err != nil { + t.Fatal("Failed to read from /tmp/cup:", err) + } + if string(fluid) != "i am a teacup" { + t.Fatal("Wrong contents in cup. got:", string(fluid)) + } +} + +func TestDirectorySymlink(t *testing.T) { + fs := Create() + + if err := vfs.MkdirAll(fs, "/foo/a/b", 0755); err != nil { + t.Fatal("Unable mkdir /foo/a/b:", err) + } + + if err := vfs.WriteFile(fs, "/foo/a/b/c", []byte("I can \"c\" clearly now"), 0644); err != nil { + t.Fatal("Unable to write /foo/a/b/c:", err) + } + + if err := fs.Symlink("/foo/a/b", "/foo/also_b"); err != nil { + t.Fatal("Unable to symlink /foo/also_b -> /foo/a/b:", err) + } + + contents, err := vfs.ReadFile(fs, "/foo/also_b/c") + if err != nil { + t.Fatal("Unable to read /foo/also_b/c:", err) + } + if string(contents) != "I can \"c\" clearly now" { + t.Fatal("Unexpected contents read from c:", err) + } +} + +func TestMultipleAndRelativeSymlinks(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + + for _, fsEntry := range []struct { + name, link, content string + }{ + {name: "a/b", link: "real_b"}, + {name: "a/b/c", link: "real_c"}, + {name: "a/b/c/real_d", content: "Lah dee dah"}, + {name: "a/b/c/d", link: "real_d"}, + {name: "a/d", link: "b/c/d"}, + } { + if fsEntry.link != "" { + if err := fs.Symlink(fsEntry.link, fsEntry.name); err != nil { + t.Fatalf("Unable to symlink %s -> %s: %v", fsEntry.name, fsEntry.link, err) + } + } else if fsEntry.content != "" { + if err := vfs.WriteFile(fs, fsEntry.name, []byte(fsEntry.content), 0644); err != nil { + t.Fatalf("Unable to write %s: %v", fsEntry.name, err) + } + } + } + + for _, fn := range []string{ + "a/b/c/d", + "a/d", + } { + contents, err := vfs.ReadFile(fs, fn) + if err != nil { + t.Fatalf("Unable to read %s: %v", fn, err) + } + if string(contents) != "Lah dee dah" { + t.Fatalf("Unexpected contents read from %s: %v", fn, err) + } + } +} + +func TestSymlinkIsNotADirectory(t *testing.T) { + fs := Create() + if err := vfs.MkdirAll(fs, "a/real_b/real_c", 0755); err != nil { + t.Fatal("Unable mkdir a/real_b/real_c:", err) + } + if err := fs.Symlink("broken", "a/b"); err != nil { + t.Fatal("Unable to symlink a/b -> broken:", err) + } + if err := vfs.WriteFile(fs, "a/b/c", []byte("Whatever"), 0644); !strings.Contains(err.Error(), vfs.ErrNotDirectory.Error()) { + t.Fatal("Expected an error when writing a/b/c:", err) + } +} + +// TODO: overwrite/remove symlinks + func TestReadDir(t *testing.T) { fs := Create() dirs := []string{"/home", "/home/linus", "/home/rob", "/home/pike", "/home/blang"} diff --git a/mountfs/mountfs.go b/mountfs/mountfs.go index aa0396c..9e7f829 100644 --- a/mountfs/mountfs.go +++ b/mountfs/mountfs.go @@ -127,6 +127,16 @@ func (fs MountFS) Mkdir(name string, perm os.FileMode) error { return mount.Mkdir(innerPath, perm) } +// Symlink creates a symlink +func (fs MountFS) Symlink(oldname, newname string) error { + oldMount, oldInnerName := findMount(oldname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + newMount, newInnerName := findMount(newname, fs.mounts, fs.rootFS, string(fs.PathSeparator())) + if oldMount != newMount { + return ErrBoundary + } + return oldMount.Symlink(oldInnerName, newInnerName) +} + type innerFileInfo struct { os.FileInfo name string diff --git a/os.go b/os.go index feb33bb..24ba320 100644 --- a/os.go +++ b/os.go @@ -33,6 +33,11 @@ func (fs OsFS) Mkdir(name string, perm os.FileMode) error { return os.Mkdir(name, perm) } +// Symlink wraps os.Symlink +func (fs OsFS) Symlink(oldname, newname string) error { + return os.Symlink(oldname, newname) +} + // Rename wraps os.Rename func (fs OsFS) Rename(oldpath, newpath string) error { return os.Rename(oldpath, newpath) diff --git a/prefixfs/prefixfs.go b/prefixfs/prefixfs.go index 6b84dfb..359121f 100644 --- a/prefixfs/prefixfs.go +++ b/prefixfs/prefixfs.go @@ -16,7 +16,7 @@ type FS struct { // Create returns a file system that prefixes all paths and forwards to root. func Create(root vfs.Filesystem, prefix string) *FS { - return &FS{root, prefix} + return &FS{Filesystem: root, Prefix: prefix} } // PrefixPath returns path with the prefix prefixed. @@ -47,6 +47,11 @@ func (fs *FS) Mkdir(name string, perm os.FileMode) error { return fs.Filesystem.Mkdir(fs.PrefixPath(name), perm) } +// Symlink implements vfs.Filesystem. +func (fs *FS) Symlink(oldname, newname string) error { + return fs.Filesystem.Symlink(fs.PrefixPath(oldname), fs.PrefixPath(newname)) +} + // Stat implements vfs.Filesystem. func (fs *FS) Stat(name string) (os.FileInfo, error) { return fs.Filesystem.Stat(fs.PrefixPath(name)) diff --git a/prefixfs/prefixfs_test.go b/prefixfs/prefixfs_test.go index c04e746..8cbd041 100644 --- a/prefixfs/prefixfs_test.go +++ b/prefixfs/prefixfs_test.go @@ -103,6 +103,27 @@ func TestMkdir(t *testing.T) { } } +func TestSymlink(t *testing.T) { + rfs := rootfs() + fs := Create(rfs, prefixPath) + + f, err := fs.OpenFile("file", os.O_CREATE, 0666) + defer f.Close() + if err != nil { + t.Errorf("OpenFile: %v", err) + } + + err = fs.Symlink("/file", "file2") + if err != nil { + t.Errorf("Symlink: %v", err) + } + + _, err = rfs.Stat(prefix("file2")) + if os.IsNotExist(err) { + t.Errorf("root:%v not found (%v)", prefix("file2"), err) + } +} + func TestStat(t *testing.T) { rfs := rootfs() fs := Create(rfs, prefixPath) diff --git a/readonly.go b/readonly.go index 406cd84..6c49679 100644 --- a/readonly.go +++ b/readonly.go @@ -44,6 +44,10 @@ func (fs RoFS) Mkdir(name string, perm os.FileMode) error { return ErrReadOnly } +func (fs RoFS) Symlink(oldname, newname string) error { + return ErrReadOnly +} + // OpenFile returns ErrorReadOnly if flag contains os.O_CREATE, os.O_APPEND, os.O_WRONLY. // Otherwise it returns a read-only File with disabled Write(..) operation. func (fs RoFS) OpenFile(name string, flag int, perm os.FileMode) (File, error) { diff --git a/readonly_test.go b/readonly_test.go index f1ca36e..dffc1c6 100644 --- a/readonly_test.go +++ b/readonly_test.go @@ -66,6 +66,13 @@ func TestMkDir(t *testing.T) { } } +func TestROSymlink(t *testing.T) { + err := ro.Symlink("old", "new") + if err != ErrReadOnly { + t.Errorf("Symlink error expected") + } +} + type writeDummyFS struct { Filesystem }