Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

symlink support #21

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions dummy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion filesystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
82 changes: 58 additions & 24 deletions memfs/memfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down
102 changes: 102 additions & 0 deletions memfs/memfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package memfs
import (
"io/ioutil"
"os"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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"}
Expand Down
10 changes: 10 additions & 0 deletions mountfs/mountfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions os.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion prefixfs/prefixfs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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))
Expand Down
21 changes: 21 additions & 0 deletions prefixfs/prefixfs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions readonly.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
7 changes: 7 additions & 0 deletions readonly_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down