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 18, 2024
1 parent 7d88e59 commit 588d568
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 18 deletions.
24 changes: 20 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,20 @@ func LinkContents(src, dest string) error {
return errs.Wrap(err, "Could not resolve src and dest paths")
}

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 +47,21 @@ 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 _, 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 +71,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)
}
71 changes: 71 additions & 0 deletions internal/smartlink/smartlink_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
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, make(map[string]bool))
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)
}
6 changes: 0 additions & 6 deletions internal/smartlink/smartlink_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,10 @@ package smartlink

import (
"os"

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

// linkFile 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.Link(src, dest)
}
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, make(map[string]bool)); 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, make(map[string]bool)); err != nil {
return errs.Wrap(err, "failed to relink file")
}
case deploymentTypeCopy:
Expand Down

0 comments on commit 588d568

Please sign in to comment.