Skip to content

Instantly share code, notes, and snippets.

@var23rav
Last active October 21, 2023 15:34
Show Gist options
  • Star 14 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save var23rav/23ae5d0d4d830aff886c3c970b8f6c6b to your computer and use it in GitHub Desktop.
Save var23rav/23ae5d0d4d830aff886c3c970b8f6c6b to your computer and use it in GitHub Desktop.
GoLang: os.Rename() give error "invalid cross-device link" for Docker container with Volumes. MoveFile(source, destination) will work moving file between folders
import (
"fmt"
"io"
"os"
)
/*
GoLang: os.Rename() give error "invalid cross-device link" for Docker container with Volumes.
MoveFile(source, destination) will work moving file between folders
*/
func MoveFile(sourcePath, destPath string) error {
inputFile, err := os.Open(sourcePath)
if err != nil {
return fmt.Errorf("Couldn't open source file: %s", err)
}
outputFile, err := os.Create(destPath)
if err != nil {
inputFile.Close()
return fmt.Errorf("Couldn't open dest file: %s", err)
}
defer outputFile.Close()
_, err = io.Copy(outputFile, inputFile)
inputFile.Close()
if err != nil {
return fmt.Errorf("Writing to output file failed: %s", err)
}
// The copy was successful, so now delete the original file
err = os.Remove(sourcePath)
if err != nil {
return fmt.Errorf("Failed removing original file: %s", err)
}
return nil
}
@var23rav
Copy link
Author

This solution solves the problem by writing the temporary file to the same directory as the target while keeping the nice guarantee of atomicity, in most OS/FS, that a copy doesn't offer :) .

@nishanths
Copy link

nishanths commented Jan 31, 2022

Worth noting that the code in the gist does not attempt to preserve file attributes, file mode and permission bits, and ACLs.

@slimsag
Copy link

slimsag commented Jan 10, 2023

Better copy-based implementation that retains quick renames and file mode where possible:

func Move(source, destination string) error {
	err := os.Rename(source, destination)
	if err != nil && strings.Contains(err.Error(), "invalid cross-device link") {
		return moveCrossDevice(source, destination)
	}
	return err
}

func moveCrossDevice(source, destination string) error {
	src, err := os.Open(source)
	if err != nil {
		return errors.Wrap(err, "Open(source)")
	}
	dst, err := os.Create(destination)
	if err != nil {
		src.Close()
		return errors.Wrap(err, "Create(destination)")
	}
	_, err = io.Copy(dst, src)
	src.Close()
	dst.Close()
	if err != nil {
		return errors.Wrap(err, "Copy")
	}
	fi, err := os.Stat(source)
	if err != nil {
		os.Remove(destination)
		return errors.Wrap(err, "Stat")
	}
	err = os.Chmod(destination, fi.Mode())
	if err != nil {
		os.Remove(destination)
		return errors.Wrap(err, "Stat")
	}
	os.Remove(source)
	return nil
}

@nishanths
Copy link

nishanths commented Jan 10, 2023

A few more notes.

Error comparison:

os.Rename() give error "invalid cross-device link" for Docker container with Volumes.
strings.Contains(err.Error(), "invalid cross-device link")

If one is only targeting a Unix-like system, the check for this error may be better achieved by checking whether the syscall.Errno is 18. If one were to use the strings.Contain check as written below, in go1.19, the check will be a false negative e.g. on netbsd and darwin, where the error string is "cross-device link" (note, missing "invalid" at start).

File types/symbolic links: On Unix-like systems, when the source file is a symbolic link, the rename(2) and renameat(2) syscalls, used by os.Rename, will rename the symbolic link itself. In others words, the system calls, and consequently os.Rename, do not follow symbolic links. But in func moveCrossDevice, using os.Open will always follow symbolic links. Given this, one may likely want to do the following:

  • If the source file is a symbolic link, use os.Symlink, then remove the source file.
  • If the source file is a regular file, use os.Open, os.Create, io.Copy, then remove the source file.
  • For other file types, ???

Additionally, one may want to change os.Stat(source), which follows symbolic links, to os.Lstat(source), which doesn't follow symbolic links.

Error handling: For the (*os.File).Close and os.Remove calls in func moveCrossDevice, one may want to handle returned errors.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment