incus: Fix remote path handling on Windows

Remote instance and volume paths are always POSIX, so use "path"
rather than "path/filepath" when manipulating them.

Closes #3614

Signed-off-by: Stéphane Graber <stgraber@stgraber.org>
This commit is contained in:
Stéphane Graber 2026-07-08 20:29:00 -04:00
parent bc9cc1586d
commit a08f2fadd5
No known key found for this signature in database
GPG Key ID: C638974D64792D67
4 changed files with 47 additions and 43 deletions

View File

@ -7,6 +7,7 @@ import (
"io/fs"
"net"
"os"
"path"
"path/filepath"
"slices"
"strconv"
@ -148,7 +149,7 @@ func (c *cmdFileCreate) run(cmd *cobra.Command, args []string) error {
return errors.New(i18n.G(`Symlink target path can only be used for type "symlink"`))
}
symlinkTargetPath = filepath.Clean(symlinkTargetPath)
symlinkTargetPath = path.Clean(symlinkTargetPath)
}
if isDir {
@ -194,7 +195,7 @@ func (c *cmdFileCreate) run(cmd *cobra.Command, args []string) error {
// Create needed paths if requested
if c.file.flagMkdir {
err = sftpRecursiveMkdir(sftpConn, filepath.Dir(targetPath), nil, int64(uid), int64(gid))
err = sftpRecursiveMkdir(sftpConn, path.Dir(targetPath), nil, int64(uid), int64(gid))
if err != nil {
return err
}
@ -307,7 +308,7 @@ func (c *cmdFileDelete) run(cmd *cobra.Command, args []string) error {
err := func() error {
d := p.RemoteServer
instanceName := p.RemoteObject.List[0].String
path, _ := normalizePath(p.RemoteObject.List[1].String)
filePath, _ := normalizePath(p.RemoteObject.List[1].String)
instanceID := p.RemoteName + ":" + instanceName
sftpConn, ok := sftpClients[instanceID]
@ -321,7 +322,7 @@ func (c *cmdFileDelete) run(cmd *cobra.Command, args []string) error {
}
if c.flagForce {
err = sftpConn.RemoveAll(path)
err = sftpConn.RemoveAll(filePath)
if err != nil {
return err
}
@ -329,7 +330,7 @@ func (c *cmdFileDelete) run(cmd *cobra.Command, args []string) error {
return nil
}
return sftpConn.Remove(path)
return sftpConn.Remove(filePath)
}()
if err != nil {
errs = append(errs, err)
@ -387,7 +388,7 @@ func (c *cmdFileEdit) run(cmd *cobra.Command, args []string) error {
}
// Create temp file
f, err := os.CreateTemp("", fmt.Sprintf("incus_file_edit_*%s", filepath.Ext(fileName)))
f, err := os.CreateTemp("", fmt.Sprintf("incus_file_edit_*%s", path.Ext(fileName)))
if err != nil {
return fmt.Errorf(i18n.G("Unable to create a temporary file: %v"), err)
}
@ -524,7 +525,7 @@ func (c *cmdFilePull) pull(parsedFiles []*u.Parsed, target string) error {
err := func() error {
d := p.RemoteServer
instanceName := p.RemoteObject.List[0].String
path := p.RemoteObject.List[1].String
filePath := p.RemoteObject.List[1].String
instanceID := p.RemoteName + ":" + instanceName
sftpConn, ok := sftpClients[instanceID]
@ -537,20 +538,20 @@ func (c *cmdFilePull) pull(parsedFiles []*u.Parsed, target string) error {
sftpClients[instanceID] = sftpConn
}
srcInfo, normalizedPath, err := c.puller.statFile(sftpConn, path)
srcInfo, normalizedPath, err := c.puller.statFile(sftpConn, filePath)
if err != nil {
return err
}
// Recursively copy directories.
if srcInfo.IsDir() {
return sftpRecursivePullFile(sftpConn, srcInfo, path, normalizedPath, target, c.global.flagQuiet, c.puller.flagDereference, len(parsedFiles) > 1 || util.PathExists(target))
return sftpRecursivePullFile(sftpConn, srcInfo, filePath, normalizedPath, target, c.global.flagQuiet, c.puller.flagDereference, len(parsedFiles) > 1 || util.PathExists(target))
}
// Determine the target path.
var targetPath string
if targetIsDir {
targetPath = filepath.Join(target, filepath.Base(normalizedPath))
targetPath = filepath.Join(target, path.Base(normalizedPath))
} else {
targetPath = target
}
@ -749,7 +750,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
canProcessStdin := len(srcFiles) == 1
// Push the files
for _, path := range srcFiles {
for _, srcPath := range srcFiles {
err := func() error {
var f *os.File
var linkTarget string
@ -760,7 +761,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
Mode: mode,
}
if isStdin(path) {
if isStdin(srcPath) {
if !canProcessStdin {
return errors.New(i18n.G("stdin can only be used once, with no other source arguments"))
}
@ -772,23 +773,23 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
canProcessStdin = false
f = os.Stdin
} else {
srcInfo, wPath, err := c.pusher.statFile(path)
srcInfo, wPath, err := c.pusher.statFile(srcPath)
if err != nil {
return err
}
// Recursively copy directories.
if srcInfo.IsDir() {
return sftpRecursivePushFile(sftpConn, wPath, path, target, args, c.global.flagQuiet, c.pusher.flagDereference, len(srcFiles) > 1 || targetExists)
return sftpRecursivePushFile(sftpConn, wPath, srcPath, target, args, c.global.flagQuiet, c.pusher.flagDereference, len(srcFiles) > 1 || targetExists)
}
if srcInfo.Mode()&os.ModeSymlink != 0 {
linkTarget, err = os.Readlink(path)
linkTarget, err = os.Readlink(srcPath)
if err != nil {
return err
}
} else {
f, err = os.Open(path)
f, err = os.Open(srcPath)
if err != nil {
return fmt.Errorf(i18n.G("Failed to open source file %q: %v"), f, err)
}
@ -815,7 +816,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
// Determine the target path.
var targetPath string
if targetIsDir {
targetPath = filepath.Join(target, filepath.Base(path))
targetPath = path.Join(target, filepath.Base(srcPath))
} else {
targetPath = target
}
@ -823,7 +824,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
// Create needed paths if requested
if c.file.flagMkdir {
mode := os.FileMode(DirMode)
err = sftpRecursiveMkdir(sftpConn, filepath.Dir(targetPath), &mode, int64(args.UID), int64(args.GID))
err = sftpRecursiveMkdir(sftpConn, path.Dir(targetPath), &mode, int64(args.UID), int64(args.GID))
if err != nil {
return err
}
@ -839,7 +840,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
// Transfer the files.
progress := cli.ProgressRenderer{
Format: fmt.Sprintf(i18n.G("Pushing %s to %s: %%s"), path, targetPath),
Format: fmt.Sprintf(i18n.G("Pushing %s to %s: %%s"), srcPath, targetPath),
Quiet: c.global.flagQuiet,
}
@ -861,7 +862,7 @@ func (c *cmdFilePush) push(srcFiles []string, parsedTarget *u.Parsed) error {
}, f)
}
logger.Infof("Pushing %s to %s (%s)", path, targetPath, args.Type)
logger.Infof("Pushing %s to %s (%s)", srcPath, targetPath, args.Type)
err = sftpCreateFile(sftpConn, targetPath, args, true)
progress.Done("")
return err

View File

@ -7,6 +7,7 @@ import (
"io/fs"
"net"
"os"
"path"
"path/filepath"
"slices"
"strconv"
@ -134,7 +135,7 @@ func (c *cmdStorageVolumeFileCreate) run(cmd *cobra.Command, args []string) erro
volName := parsed[1].List[0].String
targetPath, isDir := normalizePath(parsed[1].List[1].String)
isSymlink := !parsed[2].Skipped
symlinkTargetPath := filepath.Clean(parsed[2].String)
symlinkTargetPath := path.Clean(parsed[2].String)
if !slices.Contains([]string{"file", "symlink", "directory"}, c.flagType) {
return fmt.Errorf(i18n.G("Invalid type %q"), c.flagType)
@ -187,7 +188,7 @@ func (c *cmdStorageVolumeFileCreate) run(cmd *cobra.Command, args []string) erro
// Create needed paths if requested
if c.storageVolumeFile.flagMkdir {
err := sftpRecursiveMkdir(sftpConn, filepath.Dir(targetPath), nil, int64(uid), int64(gid))
err := sftpRecursiveMkdir(sftpConn, path.Dir(targetPath), nil, int64(uid), int64(gid))
if err != nil {
return err
}
@ -466,7 +467,7 @@ func (c *cmdStorageVolumeFileEdit) run(cmd *cobra.Command, args []string) error
}
// Create temp file
f, err := os.CreateTemp("", fmt.Sprintf("incus_file_edit_*%s", filepath.Ext(fPath)))
f, err := os.CreateTemp("", fmt.Sprintf("incus_file_edit_*%s", path.Ext(fPath)))
if err != nil {
return fmt.Errorf(i18n.G("Unable to create a temporary file: %v"), err)
}
@ -609,7 +610,7 @@ func (c *cmdStorageVolumeFilePull) pull(parsedPool *u.Parsed, parsedPath *u.Pars
var targetPath string
if targetIsDir {
targetPath = filepath.Join(target, filepath.Base(normalizedPath))
targetPath = filepath.Join(target, path.Base(normalizedPath))
} else {
targetPath = target
}
@ -847,7 +848,7 @@ func (c *cmdStorageVolumeFilePush) push(srcFile string, parsedPool *u.Parsed, pa
// Determine the target path.
var targetPath string
if targetIsDir {
targetPath = filepath.Join(target, filepath.Base(srcFile))
targetPath = path.Join(target, filepath.Base(srcFile))
} else {
targetPath = target
}
@ -855,7 +856,7 @@ func (c *cmdStorageVolumeFilePush) push(srcFile string, parsedPool *u.Parsed, pa
// Create needed paths if requested
if c.storageVolumeFile.flagMkdir {
mode := os.FileMode(DirMode)
err = sftpRecursiveMkdir(sftpConn, filepath.Dir(targetPath), &mode, int64(args.UID), int64(args.GID))
err = sftpRecursiveMkdir(sftpConn, path.Dir(targetPath), &mode, int64(args.UID), int64(args.GID))
if err != nil {
return err
}

View File

@ -10,7 +10,7 @@ import (
"os"
"os/exec"
"os/signal"
"path/filepath"
"path"
"reflect"
"slices"
"sort"
@ -336,8 +336,8 @@ func unsetKey(s settable, cmd *cobra.Command, parsed []*u.Parsed) error {
return s.set(cmd, parsed)
}
func readEnvironmentFile(path string) (map[string]string, error) {
content, err := os.ReadFile(path)
func readEnvironmentFile(p string) (map[string]string, error) {
content, err := os.ReadFile(p)
if err != nil {
return nil, fmt.Errorf(i18n.G("Can't read from environment file: %w"), err)
}
@ -743,18 +743,19 @@ func formatRemote(conf *config.Config, p *u.Parsed) string {
}
// normalizePath normalizes a path and return whether it looks like a directory.
func normalizePath(path string) (string, bool) {
// On Windows, the SFTP server expects the file path to start with `/`.
path = "/" + path
return filepath.Clean(path), strings.HasSuffix(path, "/")
func normalizePath(p string) (string, bool) {
// The SFTP server expects a `/` separated path starting with `/`, so use
// the slash-only "path" logic regardless of the client platform.
p = "/" + p
return path.Clean(p), strings.HasSuffix(p, "/")
}
// isStdin returns whether the provided path looks like stdin.
func isStdin(path string) bool {
return slices.Contains([]string{"-", "/dev/stdin", "/dev/fd/0"}, path)
func isStdin(p string) bool {
return slices.Contains([]string{"-", "/dev/stdin", "/dev/fd/0"}, p)
}
// isStdout returns whether the provided path looks like stdout.
func isStdout(path string) bool {
return slices.Contains([]string{"-", "/dev/stdout", "/dev/fd/1"}, path)
func isStdout(p string) bool {
return slices.Contains([]string{"-", "/dev/stdout", "/dev/fd/1"}, p)
}

View File

@ -6,6 +6,7 @@ import (
"io"
"io/fs"
"os"
"path"
"path/filepath"
"strings"
@ -135,7 +136,7 @@ func sftpRecursivePullFile(sftpConn *sftp.Client, fInfo os.FileInfo, source stri
target := targetDir
if createRoot {
root := filepath.Base(source)
root := path.Base(source)
// `cp` has a special behavior with the following paths.
if root == "." || root == ".." {
root = ""
@ -171,7 +172,7 @@ func sftpRecursivePullFile(sftpConn *sftp.Client, fInfo os.FileInfo, source stri
}
for _, ent := range entries {
nextP := filepath.Join(normalizedSource, ent.Name())
nextP := path.Join(normalizedSource, ent.Name())
stat := sftpConn.Lstat
if dereference {
stat = sftpConn.Stat
@ -284,7 +285,7 @@ func sftpRecursivePushFile(sftpConn *sftp.Client, walkableSource string, source
}
// Prepare for file transfer
targetPath := filepath.Join(target, root, p[len(walkableSource):])
targetPath := path.Join(target, root, filepath.ToSlash(p[len(walkableSource):]))
mode, uid, gid := internalIO.GetOwnerMode(fInfo)
fileArgs := incus.InstanceFileArgs{
UID: int64(uid),
@ -394,12 +395,12 @@ func sftpRecursiveMkdir(sftpConn *sftp.Client, p string, mode *os.FileMode, uid
// Remove trailing "/" e.g. /A/B/C/. Otherwise we will end up with an
// empty array entry "" which will confuse the Mkdir() loop below.
pclean := filepath.Clean(p)
pclean := path.Clean(p)
parts := strings.Split(pclean, "/")
i := len(parts)
for ; i >= 1; i-- {
cur := filepath.Join(parts[:i]...)
cur := path.Join(parts[:i]...)
fInfo, err := sftpConn.Lstat(cur)
if err != nil {
continue
@ -414,7 +415,7 @@ func sftpRecursiveMkdir(sftpConn *sftp.Client, p string, mode *os.FileMode, uid
}
for ; i <= len(parts); i++ {
cur := filepath.Join(parts[:i]...)
cur := path.Join(parts[:i]...)
if cur == "" {
continue
}