Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Windows Hardlink & Symbolic Link Support #401

Merged
merged 4 commits into from
Oct 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/saracen/walker v0.1.4
github.com/urfave/cli/v2 v2.27.2
golang.org/x/net v0.27.0
golang.org/x/sync v0.7.0
golang.org/x/sync v0.8.0
)

require (
Expand All @@ -23,6 +23,7 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/sys v0.22.0 // indirect
golang.org/x/text v0.19.0
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys=
golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE=
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
Expand All @@ -104,6 +104,8 @@ golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9sn
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
Expand Down
6 changes: 6 additions & 0 deletions helpers_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

package main

import "path/filepath"

func toFullPath(s string) (string, error) {
return s, nil
}

func evalSymlinks(path string) (string, error) {
return filepath.EvalSymlinks(path)
}
46 changes: 45 additions & 1 deletion helpers_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@

package main

import "syscall"
import (
"os"
"path/filepath"
"strings"
"syscall"
)

func toFullPath(s string) (string, error) {
p := syscall.StringToUTF16(s)
Expand All @@ -21,3 +26,42 @@ func toFullPath(s string) (string, error) {
b = b[:n]
return syscall.UTF16ToString(b), nil
}

func evalSymlinks(path string) (string, error) {
_, err := os.Stat(path)
if err != nil {
return "", err
}

list := filepathSplitAll(path)
evaled := list[0]
for i := 1; i < len(list); i++ {
evaled = filepath.Join(evaled, list[i])

linkSrc, err := os.Readlink(evaled)
if err != nil {
// not symlink
continue
} else {
if filepath.IsAbs(linkSrc) {
evaled = linkSrc
} else {
evaled = filepath.Join(filepath.Dir(evaled), linkSrc)
}
}
}

return evaled, nil
}

func filepathSplitAll(path string) []string {
path = filepath.Clean(path)
path = filepath.ToSlash(path)

vol := filepath.VolumeName(path)

path = path[len(vol):]
list := strings.Split(path, "/")
list[0] = vol + string(filepath.Separator) + list[0]
return list
}
115 changes: 115 additions & 0 deletions helpers_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
//go:build windows

package main

import (
"bytes"
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
"testing"

"golang.org/x/text/encoding/japanese"
"golang.org/x/text/transform"
)

type testEvalSymlinksMode int

const (
testEvalSymlinksNotLink testEvalSymlinksMode = iota
testEvalSymlinksSymbolicLink
testEvalSymlinksJunction
)

func Test_evalSymlinks(t *testing.T) {
type args struct {
path string
}
tests := []struct {
name string
mode testEvalSymlinksMode
linkBasePath string
args args
want string
wantErr bool
}{
{
name: "not link",
mode: testEvalSymlinksNotLink,
args: args{
path: filepath.Join(os.TempDir(), "not_link"),
},
want: filepath.Join(os.TempDir(), "not_link"),
wantErr: false,
},
{
name: "symbolic link",
mode: testEvalSymlinksSymbolicLink,
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
args: args{
path: filepath.Join(os.TempDir(), "symbolic_link"),
},
want: filepath.Join(os.TempDir(), "link_base"),
wantErr: false,
},
{
name: "junction",
mode: testEvalSymlinksJunction,
linkBasePath: filepath.Join(os.TempDir(), "link_base"),
args: args{
path: filepath.Join(os.TempDir(), "junction"),
},
want: filepath.Join(os.TempDir(), "link_base"),
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := createLink(tt.linkBasePath, tt.args.path, tt.mode); err != nil {
t.Errorf("failed to create link: %v", err)
return
}

got, err := evalSymlinks(tt.args.path)
if (err != nil) != tt.wantErr {
t.Errorf("evalSymlinks() error = %v, wantErr %v", err, tt.wantErr)
return
}
if got != tt.want {
t.Errorf("evalSymlinks() = %v, want %v", got, tt.want)
}
})
}
}

func createLink(linkBasePath, path string, mode testEvalSymlinksMode) error {
if err := os.RemoveAll(path); err != nil {
return err
}

if mode == testEvalSymlinksNotLink {
return os.MkdirAll(path, 0755)
}

if err := os.MkdirAll(linkBasePath, 0755); err != nil {
return err
}

switch mode {
case testEvalSymlinksSymbolicLink:
return os.Symlink(linkBasePath, path)
case testEvalSymlinksJunction:
output, err := exec.Command("cmd", "/c", "mklink", "/J", path, linkBasePath).CombinedOutput()
if err != nil {
output, err := io.ReadAll(transform.NewReader(bytes.NewBuffer(output), japanese.ShiftJIS.NewDecoder()))
if err != nil {
return fmt.Errorf("failed to transform output: %w", err)
}
return fmt.Errorf("failed to create junction: %s, %w", string(output), err)
}
return nil
}
return nil
}
2 changes: 1 addition & 1 deletion local_repository.go
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,7 @@ func localRepositoryRoots(all bool) ([]string, error) {
for _, v := range roots {
path := filepath.Clean(v)
if _, err := os.Stat(path); err == nil {
if path, err = filepath.EvalSymlinks(path); err != nil {
if path, err = evalSymlinks(path); err != nil {
_localRepoErr = err
return
}
Expand Down
Loading