Skip to content

Commit da5fee5

Browse files
committed
Recurse into recursively linked directories not more than once.
This results in a destination without directory links, which are not supported on Windows.
1 parent d33a4d0 commit da5fee5

File tree

4 files changed

+34
-37
lines changed

4 files changed

+34
-37
lines changed

internal/smartlink/smartlink.go

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import (
1010
)
1111

1212
// LinkContents will link the contents of src to desc
13-
func LinkContents(src, dest string, visited map[string]bool) error {
13+
func LinkContents(src, dest string) error {
1414
if !fileutils.DirExists(src) {
1515
return errs.New("src dir does not exist: %s", src)
1616
}
@@ -24,23 +24,12 @@ func LinkContents(src, dest string, visited map[string]bool) error {
2424
return errs.Wrap(err, "Could not resolve src and dest paths")
2525
}
2626

27-
if visited == nil {
28-
visited = make(map[string]bool)
29-
}
30-
if _, exists := visited[src]; exists {
31-
// We've encountered a recursive link. This is most often the case when the resolved src has
32-
// already been visited. In that case, just link the dest to the src (which may be a directory;
33-
// this is fine).
34-
return linkFile(src, dest)
35-
}
36-
visited[src] = true
37-
3827
entries, err := os.ReadDir(src)
3928
if err != nil {
4029
return errs.Wrap(err, "Reading dir %s failed", src)
4130
}
4231
for _, entry := range entries {
43-
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), visited); err != nil {
32+
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), nil); err != nil {
4433
return errs.Wrap(err, "Link failed")
4534
}
4635
}
@@ -50,23 +39,27 @@ func LinkContents(src, dest string, visited map[string]bool) error {
5039

5140
// Link creates a link from src to target. MS decided to support Symlinks but only if you opt into developer mode (go figure),
5241
// which we cannot reasonably force on our users. So on Windows we will instead create dirs and hardlinks.
53-
func Link(src, dest string, visited map[string]bool) error {
42+
func Link(src, dest string, visited map[string]int) error {
5443
var err error
5544
src, dest, err = resolvePaths(src, dest)
5645
if err != nil {
5746
return errs.Wrap(err, "Could not resolve src and dest paths")
5847
}
5948

6049
if visited == nil {
61-
visited = make(map[string]bool)
50+
visited = make(map[string]int)
6251
}
63-
if _, exists := visited[src]; exists {
52+
if count, exists := visited[src]; exists {
6453
// We've encountered a recursive link. This is most often the case when the resolved src has
65-
// already been visited. In that case, just link the dest to the src (which may be a directory;
66-
// this is fine).
67-
return linkFile(src, dest)
54+
// already been visited. We will recurse into the directory no more than once, so that any
55+
// runtime paths that reference the link will not silently fail.
56+
if count > 1 {
57+
return nil
58+
}
59+
visited[src]++
60+
} else {
61+
visited[src] = 1
6862
}
69-
visited[src] = true
7063

7164
if fileutils.IsDir(src) {
7265
if err := fileutils.Mkdir(dest); err != nil {

internal/smartlink/smartlink_lin_mac.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,16 @@ package smartlink
55

66
import (
77
"os"
8+
9+
"github.com/ActiveState/cli/internal/errs"
10+
"github.com/ActiveState/cli/internal/fileutils"
811
)
912

1013
// file will create a symlink from src to dest, and falls back on a hardlink if no symlink is available.
1114
// This is a workaround for the fact that Windows does not support symlinks without admin privileges.
1215
func linkFile(src, dest string) error {
16+
if fileutils.IsDir(src) {
17+
return errs.New("src is a directory, not a file: %s", src)
18+
}
1319
return os.Symlink(src, dest)
1420
}

internal/smartlink/smartlink_test.go

Lines changed: 14 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,8 @@ package smartlink
33
import (
44
"os"
55
"path/filepath"
6-
"runtime"
76
"testing"
87

9-
"github.com/ActiveState/cli/internal/fileutils"
108
"github.com/stretchr/testify/require"
119
)
1210

@@ -42,14 +40,17 @@ func TestLinkContentsWithCircularLink(t *testing.T) {
4240
err = os.Symlink(subDir, circularLink)
4341
require.NoError(t, err)
4442

45-
err = LinkContents(srcDir, destDir, nil)
46-
if runtime.GOOS == "windows" {
47-
require.Error(t, err)
48-
return // hard links to directories are not allowed on Windows
49-
}
43+
err = LinkContents(srcDir, destDir)
5044
require.NoError(t, err)
5145

5246
// Verify file structure.
47+
// src/
48+
// ├── regular.txt
49+
// └── subdir/
50+
// ├── circle
51+
// │ │ (no subdir/)
52+
// │ └── subfile.txt
53+
// └── subfile.txt
5354
destFile := filepath.Join(destDir, "regular.txt")
5455
require.FileExists(t, destFile)
5556
content, err := os.ReadFile(destFile)
@@ -62,14 +63,11 @@ func TestLinkContentsWithCircularLink(t *testing.T) {
6263
require.NoError(t, err)
6364
require.Equal(t, "sub content", string(subContent))
6465

65-
destCircular := filepath.Join(destDir, "subdir", "circle")
66-
require.FileExists(t, destCircular)
67-
target, err := fileutils.ResolveUniquePath(destCircular)
66+
require.NoDirExists(t, filepath.Join(destDir, "subdir", "circle", "circle"))
67+
68+
destCircularSubFile := filepath.Join(destDir, "subdir", "circle", "subfile.txt")
69+
require.FileExists(t, destCircularSubFile)
70+
subContent, err = os.ReadFile(destCircularSubFile)
6871
require.NoError(t, err)
69-
srcCircular := filepath.Join(srcDir, "subdir")
70-
if runtime.GOOS == "darwin" {
71-
srcCircular, err = fileutils.ResolveUniquePath(srcCircular) // needed for full $TMPDIR resolution
72-
require.NoError(t, err)
73-
}
74-
require.Equal(t, target, srcCircular)
72+
require.Equal(t, "sub content", string(subContent))
7573
}

pkg/runtime/depot.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
177177
}
178178

179179
// Copy or link the artifact files, depending on whether the artifact in question relies on file transformations
180-
if err := smartlink.LinkContents(absoluteSrc, absoluteDest, nil); err != nil {
180+
if err := smartlink.LinkContents(absoluteSrc, absoluteDest); err != nil {
181181
return errs.Wrap(err, "failed to link artifact")
182182
}
183183

0 commit comments

Comments
 (0)