Skip to content

Commit 7a2368f

Browse files
authored
Merge pull request #33 from mcuadros/memfs-re-symlink
memfs: better symlink support + *: lstat method
2 parents 98a5a28 + 51f7830 commit 7a2368f

File tree

6 files changed

+139
-74
lines changed

6 files changed

+139
-74
lines changed

fs.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,12 @@ type Filesystem interface {
3131
Create(filename string) (File, error)
3232
Open(filename string) (File, error)
3333
OpenFile(filename string, flag int, perm os.FileMode) (File, error)
34+
// Stat returns a FileInfo describing the named file.
3435
Stat(filename string) (FileInfo, error)
36+
// Lstat returns a FileInfo describing the named file. If the file is a
37+
// symbolic link, the returned FileInfo describes the symbolic link. Lstat
38+
// makes no attempt to follow the link.
39+
Lstat(filename string) (FileInfo, error)
3540
ReadDir(path string) ([]FileInfo, error)
3641
TempFile(dir, prefix string) (File, error)
3742
Rename(from, to string) error

memfs/memory.go

Lines changed: 74 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,13 @@ type Memory struct {
2121
s *storage
2222

2323
tempCount int
24-
links map[string]internalLink
25-
}
26-
27-
type internalLink struct {
28-
Name, Target string
2924
}
3025

3126
//New returns a new Memory filesystem
3227
func New() *Memory {
3328
return &Memory{
34-
base: string(separator),
35-
s: newStorage(),
36-
links: make(map[string]internalLink),
29+
base: string(separator),
30+
s: newStorage(),
3731
}
3832
}
3933

@@ -49,44 +43,51 @@ func (fs *Memory) Open(filename string) (billy.File, error) {
4943

5044
// OpenFile returns the file from a given name with given flag and permits.
5145
func (fs *Memory) OpenFile(filename string, flag int, perm os.FileMode) (billy.File, error) {
52-
path := fs.resolvePath(filename)
53-
f, has := fs.s.Get(path)
46+
fullpath := fs.fullpath(filename)
47+
f, has := fs.s.Get(fullpath)
5448
if !has {
5549
if !isCreate(flag) {
5650
return nil, os.ErrNotExist
5751
}
5852

5953
var err error
60-
f, err = fs.s.New(path, perm, flag)
54+
f, err = fs.s.New(fullpath, perm, flag)
6155
if err != nil {
6256
return nil, err
6357
}
58+
} else {
59+
if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
60+
return fs.OpenFile(target, flag, perm)
61+
}
6462
}
6563

6664
if f.mode.IsDir() {
6765
return nil, fmt.Errorf("cannot open directory: %s", filename)
6866
}
6967

70-
filename, err := filepath.Rel(fs.base, path)
68+
filename, err := filepath.Rel(fs.base, fullpath)
7169
if err != nil {
7270
return nil, err
7371
}
7472

7573
return f.Duplicate(filename, perm, flag), nil
7674
}
7775

78-
func (fs *Memory) resolvePath(path string) string {
79-
fullpath := clean(fs.Join(fs.base, path))
80-
l, ok := fs.links[fullpath]
81-
if !ok {
82-
return fullpath
76+
func (fs *Memory) fullpath(path string) string {
77+
return clean(fs.Join(fs.base, path))
78+
}
79+
80+
func (fs *Memory) resolveIfLink(fullpath string, f *file) (target string, isLink bool) {
81+
if !isSymlink(f.mode) {
82+
return fullpath, false
8383
}
8484

85-
if isAbs(l.Target) {
86-
return l.Target
85+
target = string(f.content.bytes)
86+
if !isAbs(target) {
87+
target = fs.Join(filepath.Dir(fullpath), target)
8788
}
8889

89-
return fs.resolvePath(fs.Join(filepath.Dir(fullpath), l.Target))
90+
return clean(target), true
9091
}
9192

9293
// On Windows OS, IsAbs validates if a path is valid based on if stars with a
@@ -98,28 +99,50 @@ func isAbs(path string) bool {
9899

99100
// Stat returns a billy.FileInfo with the information of the requested file.
100101
func (fs *Memory) Stat(filename string) (billy.FileInfo, error) {
101-
fullpath := fs.resolvePath(filename)
102+
fullpath := fs.fullpath(filename)
102103
f, has := fs.s.Get(fullpath)
103104
if !has {
104105
return nil, os.ErrNotExist
105106
}
106107

107-
fi := f.Stat().(*fileInfo)
108+
fi := f.Stat()
109+
110+
var err error
111+
if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
112+
fi, err = fs.Stat(target)
113+
if err != nil {
114+
return nil, err
115+
}
116+
}
108117

109118
// the name of the file should always the name of the stated file, so we
110119
// overwrite the Stat returned from the storage with it, since the
111120
// filename may belong to a link.
112-
fi.name = filepath.Base(filename)
113-
121+
fi.(*fileInfo).name = filepath.Base(filename)
114122
return fi, nil
115123
}
116124

125+
func (fs *Memory) Lstat(filename string) (billy.FileInfo, error) {
126+
fullpath := fs.fullpath(filename)
127+
f, has := fs.s.Get(fullpath)
128+
if !has {
129+
return nil, os.ErrNotExist
130+
}
131+
132+
return f.Stat(), nil
133+
}
134+
117135
// ReadDir returns a list of billy.FileInfo in the given directory.
118136
func (fs *Memory) ReadDir(path string) ([]billy.FileInfo, error) {
119-
path = fs.resolvePath(path)
137+
fullpath := fs.fullpath(path)
138+
if f, has := fs.s.Get(fullpath); has {
139+
if target, isLink := fs.resolveIfLink(fullpath, f); isLink {
140+
return fs.ReadDir(target)
141+
}
142+
}
120143

121144
var entries []billy.FileInfo
122-
for _, f := range fs.s.Children(path) {
145+
for _, f := range fs.s.Children(fullpath) {
123146
entries = append(entries, f.Stat())
124147
}
125148

@@ -161,49 +184,18 @@ func (fs *Memory) getTempFilename(dir, prefix string) string {
161184

162185
// Rename moves a the `from` file to the `to` file.
163186
func (fs *Memory) Rename(from, to string) error {
164-
if fs.renameIfLink(from, to) {
165-
return nil
166-
}
167-
168187
from = fs.Join(fs.base, from)
169188
to = fs.Join(fs.base, to)
170189

171190
return fs.s.Rename(from, to)
172191
}
173192

174-
func (fs *Memory) renameIfLink(from, to string) bool {
175-
from = clean(fs.Join(fs.base, from))
176-
to = clean(fs.Join(fs.base, to))
177-
178-
if _, ok := fs.links[from]; !ok {
179-
return false
180-
}
181-
182-
fs.links[to] = fs.links[from]
183-
delete(fs.links, from)
184-
return true
185-
}
186-
187193
// Remove deletes a given file from storage.
188194
func (fs *Memory) Remove(filename string) error {
189-
if fs.removeIfLink(filename) {
190-
return nil
191-
}
192-
193195
fullpath := fs.Join(fs.base, filename)
194196
return fs.s.Remove(fullpath)
195197
}
196198

197-
func (fs *Memory) removeIfLink(filename string) bool {
198-
fullpath := clean(fs.Join(fs.base, filename))
199-
if _, ok := fs.links[fullpath]; !ok {
200-
return false
201-
}
202-
203-
delete(fs.links, fullpath)
204-
return true
205-
}
206-
207199
// Join joins any number of path elements into a single path, adding a Separator if necessary.
208200
func (fs *Memory) Join(elem ...string) string {
209201
return filepath.Join(elem...)
@@ -219,26 +211,37 @@ func (fs *Memory) Dir(path string) billy.Filesystem {
219211
}
220212

221213
func (fs *Memory) Symlink(target, link string) error {
222-
if _, err := fs.Stat(link); err == nil {
214+
fullpath := clean(fs.Join(fs.base, link))
215+
216+
_, err := fs.Stat(fullpath)
217+
if err == nil {
223218
return os.ErrExist
224219
}
225220

226-
fullpath := clean(fs.Join(fs.base, link))
227-
fs.links[fullpath] = internalLink{
228-
Name: link,
229-
Target: clean(target),
221+
if !os.IsNotExist(err) {
222+
return err
230223
}
231224

232-
return nil
225+
target = clean(target)
226+
return billy.WriteFile(fs, fullpath, []byte(target), 0777|os.ModeSymlink)
233227
}
234228

235229
func (fs *Memory) Readlink(link string) (string, error) {
236-
fullpath := clean(fs.Join(fs.base, link))
237-
if l, ok := fs.links[fullpath]; ok {
238-
return l.Target, nil
230+
fullpath := fs.fullpath(link)
231+
f, has := fs.s.Get(fullpath)
232+
if !has {
233+
return "", os.ErrNotExist
239234
}
240235

241-
return "", os.ErrNotExist
236+
if !isSymlink(f.mode) {
237+
return "", &os.PathError{
238+
Op: "readlink",
239+
Path: fullpath,
240+
Err: fmt.Errorf("not a symlink"),
241+
}
242+
}
243+
244+
return string(f.content.bytes), nil
242245
}
243246

244247
// Base returns the base path for the filesystem.
@@ -409,3 +412,7 @@ func isReadOnly(flag int) bool {
409412
func isWriteOnly(flag int) bool {
410413
return flag&os.O_WRONLY != 0
411414
}
415+
416+
func isSymlink(m os.FileMode) bool {
417+
return m&os.ModeSymlink != 0
418+
}

osfs/os.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,11 @@ func (fs *OS) RemoveAll(path string) error {
164164
return os.RemoveAll(fullpath)
165165
}
166166

167+
func (fs *OS) Lstat(filename string) (billy.FileInfo, error) {
168+
fullpath := fs.Join(fs.base, filename)
169+
return os.Lstat(fullpath)
170+
}
171+
167172
// Symlink imlements billy.Symlinker.Symlink.
168173
func (fs *OS) Symlink(target, link string) error {
169174
target = filepath.FromSlash(target)

subdirfs/subdir.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@ func (s *subdirFs) Stat(filename string) (billy.FileInfo, error) {
9393
return newFileInfo(filepath.Base(fullpath), fi), nil
9494
}
9595

96+
func (s *subdirFs) Lstat(filename string) (billy.FileInfo, error) {
97+
fullpath := s.underlyingPath(filename)
98+
fi, err := s.underlying.Lstat(fullpath)
99+
if err != nil {
100+
return nil, err
101+
}
102+
103+
return newFileInfo(filepath.Base(fullpath), fi), nil
104+
}
105+
96106
func (s *subdirFs) ReadDir(path string) ([]billy.FileInfo, error) {
97107
prefix := s.underlyingPath(path)
98108
fis, err := s.underlying.ReadDir(prefix)

test/fs_suite.go

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,19 @@ func (s *FilesystemSuite) TestStat(c *C) {
459459
c.Assert(fi.IsDir(), Equals, false)
460460
}
461461

462+
func (s *FilesystemSuite) TestStatLink(c *C) {
463+
WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
464+
s.FS.Symlink("bar", "foo/qux")
465+
466+
fi, err := s.FS.Stat("foo/qux")
467+
c.Assert(err, IsNil)
468+
c.Assert(fi.Name(), Equals, "qux")
469+
c.Assert(fi.Size(), Equals, int64(3))
470+
c.Assert(fi.Mode(), Equals, customMode)
471+
c.Assert(fi.ModTime().IsZero(), Equals, false)
472+
c.Assert(fi.IsDir(), Equals, false)
473+
}
474+
462475
func (s *FilesystemSuite) TestStatDir(c *C) {
463476
s.FS.MkdirAll("foo/bar", 0644)
464477

@@ -477,6 +490,30 @@ func (s *FilesystemSuite) TestStatNonExistent(c *C) {
477490
c.Assert(fi, IsNil)
478491
}
479492

493+
func (s *FilesystemSuite) TestLstat(c *C) {
494+
WriteFile(s.FS, "foo/bar", []byte("foo"), customMode)
495+
496+
fi, err := s.FS.Lstat("foo/bar")
497+
c.Assert(err, IsNil)
498+
c.Assert(fi.Name(), Equals, "bar")
499+
c.Assert(fi.Size(), Equals, int64(3))
500+
c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, false)
501+
c.Assert(fi.ModTime().IsZero(), Equals, false)
502+
c.Assert(fi.IsDir(), Equals, false)
503+
}
504+
505+
func (s *FilesystemSuite) TestLstatLink(c *C) {
506+
WriteFile(s.FS, "foo/bar", []byte("fosddddaaao"), customMode)
507+
s.FS.Symlink("bar", "foo/qux")
508+
509+
fi, err := s.FS.Lstat("foo/qux")
510+
c.Assert(err, IsNil)
511+
c.Assert(fi.Name(), Equals, "qux")
512+
c.Assert(fi.Mode()&os.ModeSymlink != 0, Equals, true)
513+
c.Assert(fi.ModTime().IsZero(), Equals, false)
514+
c.Assert(fi.IsDir(), Equals, false)
515+
}
516+
480517
func (s *FilesystemSuite) TestDirStat(c *C) {
481518
files := []string{"foo", "bar", "qux/baz", "qux/qux"}
482519
for _, name := range files {
@@ -938,8 +975,6 @@ func (s *FilesystemSuite) TestSymlinkBasic(c *C) {
938975
fi, err := s.FS.Stat("link")
939976
c.Assert(err, IsNil)
940977
c.Assert(fi.Name(), Equals, "link")
941-
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
942-
c.Assert(fi.Size(), Equals, int64(0))
943978
}
944979

945980
func (s *FilesystemSuite) TestSymlinkCrossDirs(c *C) {
@@ -952,8 +987,6 @@ func (s *FilesystemSuite) TestSymlinkCrossDirs(c *C) {
952987
fi, err := s.FS.Stat("bar/link")
953988
c.Assert(err, IsNil)
954989
c.Assert(fi.Name(), Equals, "link")
955-
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
956-
c.Assert(fi.Size(), Equals, int64(0))
957990
}
958991

959992
func (s *FilesystemSuite) TestSymlinkLinkToLink(c *C) {
@@ -969,8 +1002,6 @@ func (s *FilesystemSuite) TestSymlinkLinkToLink(c *C) {
9691002
fi, err := s.FS.Stat("linkB")
9701003
c.Assert(err, IsNil)
9711004
c.Assert(fi.Name(), Equals, "linkB")
972-
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
973-
c.Assert(fi.Size(), Equals, int64(0))
9741005
}
9751006

9761007
func (s *FilesystemSuite) TestSymlinkToDir(c *C) {
@@ -983,7 +1014,6 @@ func (s *FilesystemSuite) TestSymlinkToDir(c *C) {
9831014
fi, err := s.FS.Stat("link")
9841015
c.Assert(err, IsNil)
9851016
c.Assert(fi.Name(), Equals, "link")
986-
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
9871017
c.Assert(fi.IsDir(), Equals, true)
9881018
}
9891019

tmpoverlayfs/tmpfs.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -152,6 +152,14 @@ func (t *tmpFs) Stat(path string) (billy.FileInfo, error) {
152152
return t.fs.Stat(path)
153153
}
154154

155+
func (t *tmpFs) Lstat(path string) (billy.FileInfo, error) {
156+
if t.isTmpFile(path) {
157+
return t.tmp.Lstat(path)
158+
}
159+
160+
return t.fs.Lstat(path)
161+
}
162+
155163
func (t *tmpFs) isTmpFile(p string) bool {
156164
p = path.Clean(p)
157165
_, ok := t.tempFiles[p]

0 commit comments

Comments
 (0)