346 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			Go
		
	
	
	
| // Copyright 2016 The Go Authors. All rights reserved.
 | |
| // Use of this source code is governed by a BSD-style
 | |
| // license that can be found in the LICENSE file.
 | |
| 
 | |
| package fs
 | |
| 
 | |
| import (
 | |
| 	"errors"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"runtime"
 | |
| 	"syscall"
 | |
| )
 | |
| 
 | |
| // RenameWithFallback attempts to rename a file or directory, but falls back to
 | |
| // copying in the event of a cross-device link error. If the fallback copy
 | |
| // succeeds, src is still removed, emulating normal rename behavior.
 | |
| func RenameWithFallback(src, dst string) error {
 | |
| 	_, err := os.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("cannot stat %s: %w", src, err)
 | |
| 	}
 | |
| 
 | |
| 	err = os.Rename(src, dst)
 | |
| 	if err == nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	return renameFallback(err, src, dst)
 | |
| }
 | |
| 
 | |
| // renameByCopy attempts to rename a file or directory by copying it to the
 | |
| // destination and then removing the src thus emulating the rename behavior.
 | |
| func renameByCopy(src, dst string) error {
 | |
| 	var cerr error
 | |
| 	if dir, _ := IsDir(src); dir {
 | |
| 		cerr = CopyDir(src, dst)
 | |
| 		if cerr != nil {
 | |
| 			cerr = fmt.Errorf("copying directory failed: %w", cerr)
 | |
| 		}
 | |
| 	} else {
 | |
| 		cerr = copyFile(src, dst)
 | |
| 		if cerr != nil {
 | |
| 			cerr = fmt.Errorf("copying file failed: %w", cerr)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	if cerr != nil {
 | |
| 		return fmt.Errorf("rename fallback failed: cannot rename %s to %s: %w", src, dst, cerr)
 | |
| 	}
 | |
| 
 | |
| 	if err := os.RemoveAll(src); err != nil {
 | |
| 		return fmt.Errorf("cannot delete %s: %w", src, err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	errSrcNotDir = errors.New("source is not a directory")
 | |
| 	errDstExist  = errors.New("destination already exists")
 | |
| )
 | |
| 
 | |
| // CopyDir recursively copies a directory tree, attempting to preserve permissions.
 | |
| // Source directory must exist, destination directory must *not* exist.
 | |
| func CopyDir(src, dst string) error {
 | |
| 	src = filepath.Clean(src)
 | |
| 	dst = filepath.Clean(dst)
 | |
| 
 | |
| 	// We use os.Lstat() here to ensure we don't fall in a loop where a symlink
 | |
| 	// actually links to a one of its parent directories.
 | |
| 	fi, err := os.Lstat(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if !fi.IsDir() {
 | |
| 		return errSrcNotDir
 | |
| 	}
 | |
| 
 | |
| 	_, err = os.Stat(dst)
 | |
| 	if err != nil && !os.IsNotExist(err) {
 | |
| 		return err
 | |
| 	}
 | |
| 	if err == nil {
 | |
| 		return errDstExist
 | |
| 	}
 | |
| 
 | |
| 	if err = os.MkdirAll(dst, fi.Mode()); err != nil {
 | |
| 		return fmt.Errorf("cannot mkdir %s: %w", dst, err)
 | |
| 	}
 | |
| 
 | |
| 	entries, err := os.ReadDir(src)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("cannot read directory %s: %w", dst, err)
 | |
| 	}
 | |
| 
 | |
| 	for _, entry := range entries {
 | |
| 		srcPath := filepath.Join(src, entry.Name())
 | |
| 		dstPath := filepath.Join(dst, entry.Name())
 | |
| 
 | |
| 		if entry.IsDir() {
 | |
| 			if err = CopyDir(srcPath, dstPath); err != nil {
 | |
| 				return fmt.Errorf("copying directory failed: %w", err)
 | |
| 			}
 | |
| 		} else {
 | |
| 			// This will include symlinks, which is what we want when
 | |
| 			// copying things.
 | |
| 			if err = copyFile(srcPath, dstPath); err != nil {
 | |
| 				return fmt.Errorf("copying file failed: %w", err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // copyFile copies the contents of the file named src to the file named
 | |
| // by dst. The file will be created if it does not already exist. If the
 | |
| // destination file exists, all its contents will be replaced by the contents
 | |
| // of the source file. The file mode will be copied from the source.
 | |
| func copyFile(src, dst string) (err error) {
 | |
| 	if sym, err := IsSymlink(src); err != nil {
 | |
| 		return fmt.Errorf("symlink check failed: %w", err)
 | |
| 	} else if sym {
 | |
| 		if err := cloneSymlink(src, dst); err != nil {
 | |
| 			if runtime.GOOS == "windows" {
 | |
| 				// If cloning the symlink fails on Windows because the user
 | |
| 				// does not have the required privileges, ignore the error and
 | |
| 				// fall back to copying the file contents.
 | |
| 				//
 | |
| 				// ERROR_PRIVILEGE_NOT_HELD is 1314 (0x522):
 | |
| 				// https://msdn.microsoft.com/en-us/library/windows/desktop/ms681385(v=vs.85).aspx
 | |
| 				if lerr, ok := err.(*os.LinkError); ok && lerr.Err != syscall.Errno(1314) {
 | |
| 					return err
 | |
| 				}
 | |
| 			} else {
 | |
| 				return err
 | |
| 			}
 | |
| 		} else {
 | |
| 			return nil
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	in, err := os.Open(src)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 	defer in.Close()
 | |
| 
 | |
| 	out, err := os.Create(dst)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	if _, err = io.Copy(out, in); err != nil {
 | |
| 		out.Close()
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Check for write errors on Close
 | |
| 	if err = out.Close(); err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	si, err := os.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return
 | |
| 	}
 | |
| 
 | |
| 	// Temporary fix for Go < 1.9
 | |
| 	//
 | |
| 	// See: https://github.com/golang/dep/issues/774
 | |
| 	// and https://github.com/golang/go/issues/20829
 | |
| 	if runtime.GOOS == "windows" {
 | |
| 		dst = fixLongPath(dst)
 | |
| 	}
 | |
| 	err = os.Chmod(dst, si.Mode())
 | |
| 
 | |
| 	return
 | |
| }
 | |
| 
 | |
| // cloneSymlink will create a new symlink that points to the resolved path of sl.
 | |
| // If sl is a relative symlink, dst will also be a relative symlink.
 | |
| func cloneSymlink(sl, dst string) error {
 | |
| 	resolved, err := os.Readlink(sl)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return os.Symlink(resolved, dst)
 | |
| }
 | |
| 
 | |
| // IsDir determines is the path given is a directory or not.
 | |
| func IsDir(name string) (bool, error) {
 | |
| 	fi, err := os.Stat(name)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 	if !fi.IsDir() {
 | |
| 		return false, fmt.Errorf("%q is not a directory", name)
 | |
| 	}
 | |
| 	return true, nil
 | |
| }
 | |
| 
 | |
| // IsSymlink determines if the given path is a symbolic link.
 | |
| func IsSymlink(path string) (bool, error) {
 | |
| 	l, err := os.Lstat(path)
 | |
| 	if err != nil {
 | |
| 		return false, err
 | |
| 	}
 | |
| 
 | |
| 	return l.Mode()&os.ModeSymlink == os.ModeSymlink, nil
 | |
| }
 | |
| 
 | |
| // fixLongPath returns the extended-length (\\?\-prefixed) form of
 | |
| // path when needed, in order to avoid the default 260 character file
 | |
| // path limit imposed by Windows. If path is not easily converted to
 | |
| // the extended-length form (for example, if path is a relative path
 | |
| // or contains .. elements), or is short enough, fixLongPath returns
 | |
| // path unmodified.
 | |
| //
 | |
| // See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
 | |
| func fixLongPath(path string) string {
 | |
| 	// Do nothing (and don't allocate) if the path is "short".
 | |
| 	// Empirically (at least on the Windows Server 2013 builder),
 | |
| 	// the kernel is arbitrarily okay with < 248 bytes. That
 | |
| 	// matches what the docs above say:
 | |
| 	// "When using an API to create a directory, the specified
 | |
| 	// path cannot be so long that you cannot append an 8.3 file
 | |
| 	// name (that is, the directory name cannot exceed MAX_PATH
 | |
| 	// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
 | |
| 	//
 | |
| 	// The MSDN docs appear to say that a normal path that is 248 bytes long
 | |
| 	// will work; empirically the path must be less then 248 bytes long.
 | |
| 	if len(path) < 248 {
 | |
| 		// Don't fix. (This is how Go 1.7 and earlier worked,
 | |
| 		// not automatically generating the \\?\ form)
 | |
| 		return path
 | |
| 	}
 | |
| 
 | |
| 	// The extended form begins with \\?\, as in
 | |
| 	// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
 | |
| 	// The extended form disables evaluation of . and .. path
 | |
| 	// elements and disables the interpretation of / as equivalent
 | |
| 	// to \. The conversion here rewrites / to \ and elides
 | |
| 	// . elements as well as trailing or duplicate separators. For
 | |
| 	// simplicity it avoids the conversion entirely for relative
 | |
| 	// paths or paths containing .. elements. For now,
 | |
| 	// \\server\share paths are not converted to
 | |
| 	// \\?\UNC\server\share paths because the rules for doing so
 | |
| 	// are less well-specified.
 | |
| 	if len(path) >= 2 && path[:2] == `\\` {
 | |
| 		// Don't canonicalize UNC paths.
 | |
| 		return path
 | |
| 	}
 | |
| 	if !isAbs(path) {
 | |
| 		// Relative path
 | |
| 		return path
 | |
| 	}
 | |
| 
 | |
| 	const prefix = `\\?`
 | |
| 
 | |
| 	pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
 | |
| 	copy(pathbuf, prefix)
 | |
| 	n := len(path)
 | |
| 	r, w := 0, len(prefix)
 | |
| 	for r < n {
 | |
| 		switch {
 | |
| 		case os.IsPathSeparator(path[r]):
 | |
| 			// empty block
 | |
| 			r++
 | |
| 		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
 | |
| 			// /./
 | |
| 			r++
 | |
| 		case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
 | |
| 			// /../ is currently unhandled
 | |
| 			return path
 | |
| 		default:
 | |
| 			pathbuf[w] = '\\'
 | |
| 			w++
 | |
| 			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
 | |
| 				pathbuf[w] = path[r]
 | |
| 				w++
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	// A drive's root directory needs a trailing \
 | |
| 	if w == len(`\\?\c:`) {
 | |
| 		pathbuf[w] = '\\'
 | |
| 		w++
 | |
| 	}
 | |
| 	return string(pathbuf[:w])
 | |
| }
 | |
| 
 | |
| func isAbs(path string) (b bool) {
 | |
| 	v := volumeName(path)
 | |
| 	if v == "" {
 | |
| 		return false
 | |
| 	}
 | |
| 	path = path[len(v):]
 | |
| 	if path == "" {
 | |
| 		return false
 | |
| 	}
 | |
| 	return os.IsPathSeparator(path[0])
 | |
| }
 | |
| 
 | |
| func volumeName(path string) (v string) {
 | |
| 	if len(path) < 2 {
 | |
| 		return ""
 | |
| 	}
 | |
| 	// with drive letter
 | |
| 	c := path[0]
 | |
| 	if path[1] == ':' &&
 | |
| 		('0' <= c && c <= '9' || 'a' <= c && c <= 'z' ||
 | |
| 			'A' <= c && c <= 'Z') {
 | |
| 		return path[:2]
 | |
| 	}
 | |
| 	// is it UNC
 | |
| 	if l := len(path); l >= 5 && os.IsPathSeparator(path[0]) && os.IsPathSeparator(path[1]) &&
 | |
| 		!os.IsPathSeparator(path[2]) && path[2] != '.' {
 | |
| 		// first, leading `\\` and next shouldn't be `\`. its server name.
 | |
| 		for n := 3; n < l-1; n++ {
 | |
| 			// second, next '\' shouldn't be repeated.
 | |
| 			if os.IsPathSeparator(path[n]) {
 | |
| 				n++
 | |
| 				// third, following something characters. its share name.
 | |
| 				if !os.IsPathSeparator(path[n]) {
 | |
| 					if path[n] == '.' {
 | |
| 						break
 | |
| 					}
 | |
| 					for ; n < l; n++ {
 | |
| 						if os.IsPathSeparator(path[n]) {
 | |
| 							break
 | |
| 						}
 | |
| 					}
 | |
| 					return path[:n]
 | |
| 				}
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return ""
 | |
| }
 |