Skip to content

Commit

Permalink
Handle recursive links in runtime sources.
Browse files Browse the repository at this point in the history
  • Loading branch information
mitchell-as committed Nov 19, 2024
1 parent fbf7ca2 commit d33a4d0
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 13 deletions.
30 changes: 26 additions & 4 deletions internal/smartlink/smartlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
)

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

if visited == nil {
visited = make(map[string]bool)
}
if _, exists := visited[src]; exists {
// We've encountered a recursive link. This is most often the case when the resolved src has
// already been visited. In that case, just link the dest to the src (which may be a directory;
// this is fine).
return linkFile(src, dest)
}
visited[src] = true

entries, err := os.ReadDir(src)
if err != nil {
return errs.Wrap(err, "Reading dir %s failed", src)
}
for _, entry := range entries {
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name())); err != nil {
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), visited); err != nil {
return errs.Wrap(err, "Link failed")
}
}
Expand All @@ -39,13 +50,24 @@ func LinkContents(src, dest string) error {

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

if visited == nil {
visited = make(map[string]bool)
}
if _, exists := visited[src]; exists {
// We've encountered a recursive link. This is most often the case when the resolved src has
// already been visited. In that case, just link the dest to the src (which may be a directory;
// this is fine).
return linkFile(src, dest)
}
visited[src] = true

if fileutils.IsDir(src) {
if err := fileutils.Mkdir(dest); err != nil {
return errs.Wrap(err, "could not create directory %s", dest)
Expand All @@ -55,7 +77,7 @@ func Link(src, dest string) error {
return errs.Wrap(err, "could not read directory %s", src)
}
for _, entry := range entries {
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name())); err != nil {
if err := Link(filepath.Join(src, entry.Name()), filepath.Join(dest, entry.Name()), visited); err != nil {
return errs.Wrap(err, "sub link failed")
}
}
Expand Down
6 changes: 0 additions & 6 deletions internal/smartlink/smartlink_lin_mac.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,10 @@ package smartlink

import (
"os"

"github.com/ActiveState/cli/internal/errs"
"github.com/ActiveState/cli/internal/fileutils"
)

// file will create a symlink from src to dest, and falls back on a hardlink if no symlink is available.
// This is a workaround for the fact that Windows does not support symlinks without admin privileges.
func linkFile(src, dest string) error {
if fileutils.IsDir(src) {
return errs.New("src is a directory, not a file: %s", src)
}
return os.Symlink(src, dest)
}
75 changes: 75 additions & 0 deletions internal/smartlink/smartlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package smartlink

import (
"os"
"path/filepath"
"runtime"
"testing"

"github.com/ActiveState/cli/internal/fileutils"
"github.com/stretchr/testify/require"
)

func TestLinkContentsWithCircularLink(t *testing.T) {
srcDir, err := os.MkdirTemp("", "src")
require.NoError(t, err)
defer os.RemoveAll(srcDir)

destDir, err := os.MkdirTemp("", "dest")
require.NoError(t, err)
defer os.RemoveAll(destDir)

// Create test file structure:
// src/
// ├── regular.txt
// └── subdir/
// ├── circle -> subdir (circular link)
// └── subfile.txt

testFile := filepath.Join(srcDir, "regular.txt")
err = os.WriteFile(testFile, []byte("test content"), 0644)
require.NoError(t, err)

subDir := filepath.Join(srcDir, "subdir")
err = os.Mkdir(subDir, 0755)
require.NoError(t, err)

subFile := filepath.Join(subDir, "subfile.txt")
err = os.WriteFile(subFile, []byte("sub content"), 0644)
require.NoError(t, err)

circularLink := filepath.Join(subDir, "circle")
err = os.Symlink(subDir, circularLink)
require.NoError(t, err)

err = LinkContents(srcDir, destDir, nil)
if runtime.GOOS == "windows" {
require.Error(t, err)
return // hard links to directories are not allowed on Windows
}
require.NoError(t, err)

// Verify file structure.
destFile := filepath.Join(destDir, "regular.txt")
require.FileExists(t, destFile)
content, err := os.ReadFile(destFile)
require.NoError(t, err)
require.Equal(t, "test content", string(content))

destSubFile := filepath.Join(destDir, "subdir", "subfile.txt")
require.FileExists(t, destSubFile)
subContent, err := os.ReadFile(destSubFile)
require.NoError(t, err)
require.Equal(t, "sub content", string(subContent))

destCircular := filepath.Join(destDir, "subdir", "circle")
require.FileExists(t, destCircular)
target, err := fileutils.ResolveUniquePath(destCircular)
require.NoError(t, err)
srcCircular := filepath.Join(srcDir, "subdir")
if runtime.GOOS == "darwin" {
srcCircular, err = fileutils.ResolveUniquePath(srcCircular) // needed for full $TMPDIR resolution
require.NoError(t, err)
}
require.Equal(t, target, srcCircular)
}
4 changes: 2 additions & 2 deletions pkg/runtime/depot.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,7 @@ func (d *depot) DeployViaLink(id strfmt.UUID, relativeSrc, absoluteDest string)
}

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

Expand Down Expand Up @@ -295,7 +295,7 @@ func (d *depot) Undeploy(id strfmt.UUID, relativeSrc, path string) error {
for sharedFile, relinkSrc := range redeploys {
switch deploy.Type {
case deploymentTypeLink:
if err := smartlink.Link(relinkSrc, sharedFile); err != nil {
if err := smartlink.Link(relinkSrc, sharedFile, nil); err != nil {
return errs.Wrap(err, "failed to relink file")
}
case deploymentTypeCopy:
Expand Down
2 changes: 1 addition & 1 deletion pkg/runtime/links_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ func supportsHardLinks(path string) (supported bool) {
}

logging.Debug("Attempting to link '%s' to '%s'", lnk, target)
err = smartlink.Link(target, lnk)
err = smartlink.Link(target, lnk, nil)
if err != nil {
logging.Debug("Test link creation failed: %v", err)
return false
Expand Down

0 comments on commit d33a4d0

Please sign in to comment.