Skip to content

Commit 81e8eae

Browse files
jmallocmcuadros
authored andcommitted
osfs: symlink support. (#30)
* Get the test suite passing on Mac OS. * Implement `Symlink()` and `Readlink()` on `osfs.OS`. * Ensure full support for relative and absolute symlinks. * Add `Symlinker` interface. * Always create parent directories when creating a symlink. * Implement `Symlink()` and `Readlink()` on `subdir.subdirFs`. * Add test for calling `Readlink()` on a regular file. * Relax specificity of error assertions. * Fix readlink assertion for windows. * Add tests for subdirfs when underlying filesystem does not support symlinks. * Document interface methods, and improve paramter names.
1 parent 5249760 commit 81e8eae

File tree

9 files changed

+359
-3
lines changed

9 files changed

+359
-3
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/coverage.txt

fs.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ type Filesystem interface {
4242
Base() string
4343
}
4444

45+
// Symlinker is a Filesystem with support for creating symlinks.
46+
type Symlinker interface {
47+
Filesystem
48+
49+
// Symlink creates a symbolic-link from link to target. target may be an
50+
// absolute or relative path, and need not refer to an existing node.
51+
// Parent directories of link are created as necessary.
52+
Symlink(target, link string) error
53+
54+
// Readlink returns the target path of link. An error is returned if link is
55+
// not a symbolic-link.
56+
Readlink(link string) (string, error)
57+
}
58+
4559
// File implements io.Closer, io.Reader, io.Seeker, and io.Writer>
4660
// Provides method to obtain the file name and the state of the file (open or closed).
4761
type File interface {

osfs/os.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Package os provides a billy filesystem for the OS.
1+
// Package osfs provides a billy filesystem for the OS.
22
package osfs // import "gopkg.in/src-d/go-billy.v2/osfs"
33

44
import (
@@ -168,6 +168,41 @@ func (fs *OS) RemoveAll(path string) error {
168168
return os.RemoveAll(fullpath)
169169
}
170170

171+
// Symlink imlements billy.Symlinker.Symlink.
172+
func (fs *OS) Symlink(target, link string) error {
173+
if filepath.IsAbs(target) {
174+
// only rewrite target if it's already absolute
175+
target = fs.Join(fs.base, target)
176+
}
177+
link = fs.Join(fs.base, link)
178+
179+
if err := fs.createDir(link); err != nil {
180+
return err
181+
}
182+
183+
return os.Symlink(target, link)
184+
}
185+
186+
// Readlink implements billy.Symlinker.Readlink.
187+
func (fs *OS) Readlink(link string) (string, error) {
188+
fullpath := fs.Join(fs.base, link)
189+
target, err := os.Readlink(fullpath)
190+
if err != nil {
191+
return "", err
192+
}
193+
194+
if !filepath.IsAbs(target) {
195+
return target, nil
196+
}
197+
198+
target, err = filepath.Rel(fs.base, target)
199+
if err != nil {
200+
return "", err
201+
}
202+
203+
return string(os.PathSeparator) + target, nil
204+
}
205+
171206
// osFile represents a file in the os filesystem
172207
type osFile struct {
173208
billy.BaseFile

subdirfs/subdir.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
package subdirfs
22

33
import (
4+
"errors"
45
"os"
56
"path/filepath"
67
"strings"
78

89
"gopkg.in/src-d/go-billy.v2"
910
)
1011

12+
// ErrSymlinkNotSupported is returned by Symlink() and Readfile() if the
13+
// underlying filesystem does not support symlinking.
14+
var ErrSymlinkNotSupported = errors.New("symlink not supported")
15+
1116
type subdirFs struct {
1217
underlying billy.Filesystem
1318
base string
@@ -113,3 +118,44 @@ func (s *subdirFs) Dir(path string) billy.Filesystem {
113118
func (s *subdirFs) Base() string {
114119
return s.base
115120
}
121+
122+
// Symlink implements billy.Symlinker.Symlink.
123+
func (s *subdirFs) Symlink(target, link string) error {
124+
fs, ok := s.underlying.(billy.Symlinker)
125+
if !ok {
126+
return ErrSymlinkNotSupported
127+
}
128+
129+
if filepath.IsAbs(target) {
130+
// only rewrite target if it's already absolute
131+
target = string(os.PathSeparator) + s.underlyingPath(target)
132+
}
133+
link = s.underlyingPath(link)
134+
return fs.Symlink(target, link)
135+
}
136+
137+
// Readlink implements billy.Symlinker.Readlink.
138+
func (s *subdirFs) Readlink(link string) (string, error) {
139+
fs, ok := s.underlying.(billy.Symlinker)
140+
if !ok {
141+
return "", ErrSymlinkNotSupported
142+
}
143+
144+
fullpath := s.underlyingPath(link)
145+
target, err := fs.Readlink(fullpath)
146+
if err != nil {
147+
return "", err
148+
}
149+
150+
if !filepath.IsAbs(target) {
151+
return target, nil
152+
}
153+
154+
base := string(os.PathSeparator) + s.base
155+
target, err = filepath.Rel(base, target)
156+
if err != nil {
157+
return "", err
158+
}
159+
160+
return string(os.PathSeparator) + target, nil
161+
}

subdirfs/subdir_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,17 @@ func (s *FilesystemSuite) TearDownTest(c *C) {
3838
err = stdos.RemoveAll(s.path)
3939
c.Assert(err, IsNil)
4040
}
41+
42+
func (s *FilesystemSuite) TestSymlinkWithNoUnderlyingSupport(c *C) {
43+
s.cfs.(*subdirFs).underlying = nil
44+
45+
err := s.cfs.(billy.Symlinker).Symlink("foo", "bar")
46+
c.Assert(err, Equals, ErrSymlinkNotSupported)
47+
}
48+
49+
func (s *FilesystemSuite) TestReadlinkWithNoUnderlyingSupport(c *C) {
50+
s.cfs.(*subdirFs).underlying = nil
51+
52+
_, err := s.cfs.(billy.Symlinker).Readlink("foo")
53+
c.Assert(err, Equals, ErrSymlinkNotSupported)
54+
}

test/fs_suite.go

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -927,3 +927,233 @@ func (s *FilesystemSuite) TestWriteFile(c *C) {
927927

928928
c.Assert(f.Close(), IsNil)
929929
}
930+
931+
func (s *FilesystemSuite) TestSymlink(c *C) {
932+
fs, ok := s.FS.(Symlinker)
933+
if !ok {
934+
c.Skip("file system does not support symlinking")
935+
}
936+
937+
err := WriteFile(fs, "file", nil, 0644)
938+
c.Assert(err, IsNil)
939+
940+
err = fs.Symlink("file", "link")
941+
c.Assert(err, IsNil)
942+
943+
fi, err := fs.Stat("link")
944+
c.Assert(err, IsNil)
945+
c.Assert(fi.Name(), Equals, "link")
946+
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
947+
c.Assert(fi.Size(), Equals, int64(0))
948+
}
949+
950+
func (s *FilesystemSuite) TestSymlinkToDir(c *C) {
951+
fs, ok := s.FS.(Symlinker)
952+
if !ok {
953+
c.Skip("file system does not support symlinking")
954+
}
955+
956+
err := fs.MkdirAll("dir", 0755)
957+
c.Assert(err, IsNil)
958+
959+
err = fs.Symlink("dir", "link")
960+
c.Assert(err, IsNil)
961+
962+
fi, err := fs.Stat("link")
963+
c.Assert(err, IsNil)
964+
c.Assert(fi.Name(), Equals, "link")
965+
c.Assert(fi.Mode()&os.ModeSymlink, Not(Equals), 0)
966+
c.Assert(fi.IsDir(), Equals, true)
967+
}
968+
969+
func (s *FilesystemSuite) TestSymlinkWithNonExistentOldname(c *C) {
970+
fs, ok := s.FS.(Symlinker)
971+
if !ok {
972+
c.Skip("file system does not support symlinking")
973+
}
974+
975+
err := fs.Symlink("file", "link")
976+
c.Assert(err, IsNil)
977+
978+
_, err = fs.Stat("link")
979+
c.Assert(os.IsNotExist(err), Equals, true)
980+
}
981+
982+
func (s *FilesystemSuite) TestSymlinkWithExistingNewname(c *C) {
983+
fs, ok := s.FS.(Symlinker)
984+
if !ok {
985+
c.Skip("file system does not support symlinking")
986+
}
987+
988+
err := WriteFile(fs, "link", nil, 0644)
989+
c.Assert(err, IsNil)
990+
991+
err = fs.Symlink("file", "link")
992+
c.Assert(err, Not(IsNil))
993+
}
994+
995+
func (s *FilesystemSuite) TestSymlinkOpenWithRelativePath(c *C) {
996+
fs, ok := s.FS.(Symlinker)
997+
if !ok {
998+
c.Skip("file system does not support symlinking")
999+
}
1000+
1001+
err := WriteFile(fs, "dir/file", []byte("foo"), 0644)
1002+
c.Assert(err, IsNil)
1003+
1004+
err = fs.Symlink("file", "dir/link")
1005+
c.Assert(err, IsNil)
1006+
1007+
f, err := s.FS.Open("dir/link")
1008+
c.Assert(err, IsNil)
1009+
1010+
all, err := ioutil.ReadAll(f)
1011+
c.Assert(err, IsNil)
1012+
c.Assert(string(all), Equals, "foo")
1013+
c.Assert(f.Close(), IsNil)
1014+
}
1015+
1016+
func (s *FilesystemSuite) TestSymlinkOpenWithAbsolutePath(c *C) {
1017+
fs, ok := s.FS.(Symlinker)
1018+
if !ok {
1019+
c.Skip("file system does not support symlinking")
1020+
}
1021+
1022+
err := WriteFile(fs, "dir/file", []byte("foo"), 0644)
1023+
c.Assert(err, IsNil)
1024+
1025+
err = fs.Symlink("/dir/file", "dir/link")
1026+
c.Assert(err, IsNil)
1027+
1028+
f, err := s.FS.Open("dir/link")
1029+
c.Assert(err, IsNil)
1030+
1031+
all, err := ioutil.ReadAll(f)
1032+
c.Assert(err, IsNil)
1033+
c.Assert(string(all), Equals, "foo")
1034+
c.Assert(f.Close(), IsNil)
1035+
}
1036+
1037+
func (s *FilesystemSuite) TestSymlinkReadDir(c *C) {
1038+
fs, ok := s.FS.(Symlinker)
1039+
if !ok {
1040+
c.Skip("file system does not support symlinking")
1041+
}
1042+
1043+
err := WriteFile(fs, "dir/file", []byte("foo"), 0644)
1044+
c.Assert(err, IsNil)
1045+
1046+
err = fs.Symlink("dir", "link")
1047+
c.Assert(err, IsNil)
1048+
1049+
info, err := s.FS.ReadDir("link")
1050+
c.Assert(err, IsNil)
1051+
c.Assert(info, HasLen, 1)
1052+
1053+
c.Assert(info[0].Size(), Equals, int64(3))
1054+
c.Assert(info[0].IsDir(), Equals, false)
1055+
c.Assert(info[0].Name(), Equals, "file")
1056+
}
1057+
1058+
func (s *FilesystemSuite) TestSymlinkRename(c *C) {
1059+
fs, ok := s.FS.(Symlinker)
1060+
if !ok {
1061+
c.Skip("file system does not support symlinking")
1062+
}
1063+
1064+
err := fs.Symlink("file", "link")
1065+
c.Assert(err, IsNil)
1066+
1067+
err = fs.Rename("link", "newlink")
1068+
c.Assert(err, IsNil)
1069+
1070+
_, err = fs.Readlink("newlink")
1071+
c.Assert(err, IsNil)
1072+
}
1073+
1074+
func (s *FilesystemSuite) TestSymlinkRemove(c *C) {
1075+
fs, ok := s.FS.(Symlinker)
1076+
if !ok {
1077+
c.Skip("file system does not support symlinking")
1078+
}
1079+
1080+
err := fs.Symlink("file", "link")
1081+
c.Assert(err, IsNil)
1082+
1083+
err = fs.Remove("link")
1084+
c.Assert(err, IsNil)
1085+
1086+
_, err = fs.Readlink("link")
1087+
c.Assert(os.IsNotExist(err), Equals, true)
1088+
}
1089+
1090+
func (s *FilesystemSuite) TestReadlinkWithRelativePath(c *C) {
1091+
fs, ok := s.FS.(Symlinker)
1092+
if !ok {
1093+
c.Skip("file system does not support symlinking")
1094+
}
1095+
1096+
err := WriteFile(fs, "dir/file", nil, 0644)
1097+
c.Assert(err, IsNil)
1098+
1099+
err = fs.Symlink("file", "dir/link")
1100+
c.Assert(err, IsNil)
1101+
1102+
oldname, err := fs.Readlink("dir/link")
1103+
c.Assert(err, IsNil)
1104+
c.Assert(oldname, Equals, "file")
1105+
}
1106+
1107+
func (s *FilesystemSuite) TestReadlinkWithAbsolutePath(c *C) {
1108+
fs, ok := s.FS.(Symlinker)
1109+
if !ok {
1110+
c.Skip("file system does not support symlinking")
1111+
}
1112+
1113+
err := WriteFile(fs, "dir/file", nil, 0644)
1114+
c.Assert(err, IsNil)
1115+
1116+
err = fs.Symlink("/dir/file", "dir/link")
1117+
c.Assert(err, IsNil)
1118+
1119+
oldname, err := fs.Readlink("dir/link")
1120+
c.Assert(err, IsNil)
1121+
c.Assert(oldname, Equals, expectedSymlinkTarget)
1122+
}
1123+
1124+
func (s *FilesystemSuite) TestReadlinkWithNonExistentOldname(c *C) {
1125+
fs, ok := s.FS.(Symlinker)
1126+
if !ok {
1127+
c.Skip("file system does not support symlinking")
1128+
}
1129+
1130+
err := fs.Symlink("file", "link")
1131+
c.Assert(err, IsNil)
1132+
1133+
oldname, err := fs.Readlink("link")
1134+
c.Assert(err, IsNil)
1135+
c.Assert(oldname, Equals, "file")
1136+
}
1137+
1138+
func (s *FilesystemSuite) TestReadlinkWithNonExistentLink(c *C) {
1139+
fs, ok := s.FS.(Symlinker)
1140+
if !ok {
1141+
c.Skip("file system does not support symlinking")
1142+
}
1143+
1144+
_, err := fs.Readlink("link")
1145+
c.Assert(os.IsNotExist(err), Equals, true)
1146+
}
1147+
1148+
func (s *FilesystemSuite) TestReadlinkWithRegularFile(c *C) {
1149+
fs, ok := s.FS.(Symlinker)
1150+
if !ok {
1151+
c.Skip("file system does not support symlinking")
1152+
}
1153+
1154+
err := WriteFile(fs, "file", nil, 0644)
1155+
c.Assert(err, IsNil)
1156+
1157+
_, err = fs.Readlink("file")
1158+
c.Assert(err, Not(IsNil))
1159+
}

test/fs_suite_darwin.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
// +build !windows
2+
3+
package test
4+
5+
import "os"
6+
7+
var (
8+
customMode os.FileMode = 0755
9+
expectedSymlinkTarget = "/dir/file"
10+
)

0 commit comments

Comments
 (0)