Skip to content
This repository was archived by the owner on Sep 9, 2020. It is now read-only.

Commit b4fca9b

Browse files
committed
gps: handle symlinks properly
We now delete anything that looks like a symlink if its called "vendor" while pruning. Hopefully, you didn't make a bad decision by relying on the magical properties of symlinks. Signed-off-by: Ibrahim AshShohail <[email protected]>
1 parent fb9ac8c commit b4fca9b

File tree

8 files changed

+457
-405
lines changed

8 files changed

+457
-405
lines changed

gps/filesystem.go

Lines changed: 72 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,25 @@ package gps
77
import (
88
"os"
99
"path/filepath"
10-
"runtime"
10+
"strings"
11+
12+
"github.com/pkg/errors"
1113
)
1214

15+
// fsLink represents a symbolic link.
16+
type fsLink struct {
17+
path string
18+
to string
19+
20+
// circular denotes if evaluating the symlink fails with "too many links" error.
21+
// This errors means that it's very likely that the symlink has circual refernce.
22+
circular bool
23+
24+
// broken denotes that attempting to resolve the link fails, most likely because
25+
// the destaination doesn't exist.
26+
broken bool
27+
}
28+
1329
// filesystemState represents the state of a file system.
1430
type filesystemState struct {
1531
root string
@@ -18,10 +34,51 @@ type filesystemState struct {
1834
links []fsLink
1935
}
2036

21-
// fsLink represents a symbolic link.
22-
type fsLink struct {
23-
path string
24-
to string
37+
func (s filesystemState) setup() error {
38+
for _, dir := range s.dirs {
39+
p := filepath.Join(s.root, dir)
40+
41+
if err := os.MkdirAll(p, 0777); err != nil {
42+
return errors.Errorf("os.MkdirAll(%q, 0777) err=%q", p, err)
43+
}
44+
}
45+
46+
for _, file := range s.files {
47+
p := filepath.Join(s.root, file)
48+
49+
f, err := os.Create(p)
50+
if err != nil {
51+
return errors.Errorf("os.Create(%q) err=%q", p, err)
52+
}
53+
54+
if err := f.Close(); err != nil {
55+
return errors.Errorf("file %q Close() err=%q", p, err)
56+
}
57+
}
58+
59+
for _, link := range s.links {
60+
p := filepath.Join(s.root, link.path)
61+
62+
// On Windows, relative symlinks confuse filepath.Walk. So, we'll just sigh
63+
// and do absolute links, assuming they are relative to the directory of
64+
// link.path.
65+
//
66+
// Reference: https://github.com/golang/go/issues/17540
67+
//
68+
// TODO(ibrasho): This was fixed in Go 1.9. Remove this when support for
69+
// 1.8 is dropped.
70+
dir := filepath.Dir(p)
71+
to := ""
72+
if link.to != "" {
73+
to = filepath.Join(dir, link.to)
74+
}
75+
76+
if err := os.Symlink(to, p); err != nil {
77+
return errors.Errorf("os.Symlink(%q, %q) err=%q", to, p, err)
78+
}
79+
}
80+
81+
return nil
2582
}
2683

2784
// deriveFilesystemState returns a filesystemState based on the state of
@@ -43,36 +100,24 @@ func deriveFilesystemState(root string) (filesystemState, error) {
43100
return err
44101
}
45102

46-
symlink := (info.Mode() & os.ModeSymlink) == os.ModeSymlink
47-
dir := info.IsDir()
48-
49-
if runtime.GOOS == "windows" && symlink && dir {
50-
// This could be a Windows junction directory. Support for these in the
51-
// standard library is spotty, and we could easily delete an important
52-
// folder if we called os.Remove or os.RemoveAll. Just skip these.
53-
//
54-
// TODO: If we could distinguish between junctions and Windows symlinks,
55-
// we might be able to safely delete symlinks, even though junctions are
56-
// dangerous.
57-
58-
return nil
59-
}
103+
if (info.Mode() & os.ModeSymlink) != 0 {
104+
l := fsLink{path: relPath}
60105

61-
if symlink {
62-
eval, err := filepath.EvalSymlinks(path)
63-
if err != nil {
106+
l.to, err = filepath.EvalSymlinks(path)
107+
if err != nil && strings.HasSuffix(err.Error(), "too many links") {
108+
l.circular = true
109+
} else if err != nil && os.IsNotExist(err) {
110+
l.broken = true
111+
} else if err != nil {
64112
return err
65113
}
66114

67-
fs.links = append(fs.links, fsLink{
68-
path: relPath,
69-
to: eval,
70-
})
115+
fs.links = append(fs.links, l)
71116

72117
return nil
73118
}
74119

75-
if dir {
120+
if info.IsDir() {
76121
fs.dirs = append(fs.dirs, relPath)
77122

78123
return nil

gps/filesystem_test.go

Lines changed: 119 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
package gps
66

77
import (
8+
"fmt"
89
"os"
910
"path/filepath"
11+
"reflect"
1012
"testing"
13+
14+
"github.com/golang/dep/internal/test"
1115
)
1216

1317
// This file contains utilities for running tests around file system state.
@@ -89,45 +93,128 @@ func (tc fsTestCase) assert(t *testing.T) {
8993
// setup inflates fs onto the actual host file system at tc.before.root.
9094
// It doesn't delete existing files and should be used on empty roots only.
9195
func (tc fsTestCase) setup(t *testing.T) {
92-
tc.setupDirs(t)
93-
tc.setupFiles(t)
94-
tc.setupLinks(t)
95-
}
96-
97-
func (tc fsTestCase) setupDirs(t *testing.T) {
98-
for _, dir := range tc.before.dirs {
99-
p := filepath.Join(tc.before.root, dir)
100-
if err := os.MkdirAll(p, 0777); err != nil {
101-
t.Fatalf("os.MkdirAll(%q, 0777) err=%q", p, err)
102-
}
96+
if err := tc.before.setup(); err != nil {
97+
t.Fatal(err)
10398
}
10499
}
105100

106-
func (tc fsTestCase) setupFiles(t *testing.T) {
107-
for _, file := range tc.before.files {
108-
p := filepath.Join(tc.before.root, file)
109-
f, err := os.Create(p)
110-
if err != nil {
111-
t.Fatalf("os.Create(%q) err=%q", p, err)
112-
}
113-
if err := f.Close(); err != nil {
114-
t.Fatalf("file %q Close() err=%q", p, err)
115-
}
101+
func TestDeriveFilesystemState(t *testing.T) {
102+
testcases := []struct {
103+
name string
104+
fs fsTestCase
105+
}{
106+
{
107+
name: "simple-case",
108+
fs: fsTestCase{
109+
before: filesystemState{
110+
dirs: []string{
111+
"simple-dir",
112+
},
113+
files: []string{
114+
"simple-file",
115+
},
116+
},
117+
after: filesystemState{
118+
dirs: []string{
119+
"simple-dir",
120+
},
121+
files: []string{
122+
"simple-file",
123+
},
124+
},
125+
},
126+
},
127+
{
128+
name: "simple-symlink-case",
129+
fs: fsTestCase{
130+
before: filesystemState{
131+
dirs: []string{
132+
"simple-dir",
133+
},
134+
files: []string{
135+
"simple-file",
136+
},
137+
links: []fsLink{
138+
fsLink{
139+
path: "link",
140+
to: "nonexisting",
141+
broken: true,
142+
},
143+
},
144+
},
145+
after: filesystemState{
146+
dirs: []string{
147+
"simple-dir",
148+
},
149+
files: []string{
150+
"simple-file",
151+
},
152+
links: []fsLink{
153+
fsLink{
154+
path: "link",
155+
to: "",
156+
broken: true,
157+
},
158+
},
159+
},
160+
},
161+
},
162+
{
163+
name: "complex-symlink-case",
164+
fs: fsTestCase{
165+
before: filesystemState{
166+
links: []fsLink{
167+
fsLink{
168+
path: "link1",
169+
to: "link2",
170+
circular: true,
171+
},
172+
fsLink{
173+
path: "link2",
174+
to: "link1",
175+
circular: true,
176+
},
177+
},
178+
},
179+
after: filesystemState{
180+
links: []fsLink{
181+
fsLink{
182+
path: "link1",
183+
to: "",
184+
circular: true,
185+
},
186+
fsLink{
187+
path: "link2",
188+
to: "",
189+
circular: true,
190+
},
191+
},
192+
},
193+
},
194+
},
116195
}
117-
}
118196

119-
func (tc fsTestCase) setupLinks(t *testing.T) {
120-
for _, link := range tc.before.links {
121-
p := filepath.Join(tc.before.root, link.path)
197+
for _, tc := range testcases {
198+
h := test.NewHelper(t)
199+
200+
h.TempDir(tc.name)
122201

123-
// On Windows, relative symlinks confuse filepath.Walk. This is golang/go
124-
// issue 17540. So, we'll just sigh and do absolute links, assuming they are
125-
// relative to the directory of link.path.
126-
dir := filepath.Dir(p)
127-
to := filepath.Join(dir, link.to)
202+
tc.fs.before.root = h.Path(tc.name)
203+
tc.fs.after.root = h.Path(tc.name)
128204

129-
if err := os.Symlink(to, p); err != nil {
130-
t.Fatalf("os.Symlink(%q, %q) err=%q", to, p, err)
205+
tc.fs.setup(t)
206+
207+
state, err := deriveFilesystemState(h.Path(tc.name))
208+
if err != nil {
209+
t.Fatal(err)
131210
}
211+
212+
if !reflect.DeepEqual(tc.fs.after, state) {
213+
fmt.Println(tc.fs.after)
214+
fmt.Println(state)
215+
t.Fatal("filesystem state mismatch")
216+
}
217+
218+
h.Cleanup()
132219
}
133220
}

gps/prune.go

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"log"
99
"os"
1010
"path/filepath"
11+
"sort"
1112
"strings"
1213

1314
"github.com/golang/dep/internal/fs"
@@ -126,11 +127,21 @@ func PruneProject(baseDir string, lp LockedProject, options PruneOptions, logger
126127

127128
// pruneVendorDirs deletes all nested vendor directories within baseDir.
128129
func pruneVendorDirs(fsState filesystemState) error {
129-
toDelete := collectNestedVendorDirs(fsState)
130+
for _, dir := range fsState.dirs {
131+
if filepath.Base(dir) == "vendor" {
132+
err := os.RemoveAll(filepath.Join(fsState.root, dir))
133+
if err != nil && !os.IsNotExist(err) {
134+
return err
135+
}
136+
}
137+
}
130138

131-
for _, path := range toDelete {
132-
if err := os.RemoveAll(path); err != nil && !os.IsNotExist(err) {
133-
return err
139+
for _, link := range fsState.links {
140+
if filepath.Base(link.path) == "vendor" {
141+
err := os.Remove(filepath.Join(fsState.root, link.path))
142+
if err != nil && !os.IsNotExist(err) {
143+
return err
144+
}
134145
}
135146
}
136147

@@ -291,6 +302,8 @@ func pruneGoTestFiles(fsState filesystemState) error {
291302
}
292303

293304
func deleteEmptyDirs(fsState filesystemState) error {
305+
toDelete := make(sort.StringSlice, 0)
306+
294307
for _, dir := range fsState.dirs {
295308
path := filepath.Join(fsState.root, dir)
296309

@@ -300,9 +313,14 @@ func deleteEmptyDirs(fsState filesystemState) error {
300313
}
301314

302315
if !notEmpty {
303-
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
304-
return err
305-
}
316+
toDelete = append(toDelete, path)
317+
}
318+
}
319+
320+
sort.Sort(sort.Reverse(sort.StringSlice(toDelete)))
321+
for _, path := range toDelete {
322+
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
323+
return err
306324
}
307325
}
308326

0 commit comments

Comments
 (0)