package shutil import ( "fmt" "io" "io/ioutil" "os" "path/filepath" ) type SameFileError struct { Src string Dst string } func (e SameFileError) Error() string { return fmt.Sprintf("%s and %s are the same file", e.Src, e.Dst) } type SpecialFileError struct { File string FileInfo os.FileInfo } func (e SpecialFileError) Error() string { return fmt.Sprintf("`%s` is a named pipe", e.File) } type NotADirectoryError struct { Src string } func (e NotADirectoryError) Error() string { return fmt.Sprintf("`%s` is not a directory", e.Src) } type AlreadyExistsError struct { Dst string } func (e AlreadyExistsError) Error() string { return fmt.Sprintf("`%s` already exists", e.Dst) } func samefile(src string, dst string) bool { srcInfo, _ := os.Stat(src) dstInfo, _ := os.Stat(dst) return os.SameFile(srcInfo, dstInfo) } func specialfile(fi os.FileInfo) bool { return (fi.Mode() & os.ModeNamedPipe) == os.ModeNamedPipe } func stringInSlice(a string, list []string) bool { for _, b := range list { if b == a { return true } } return false } func IsSymlink(fi os.FileInfo) bool { return (fi.Mode() & os.ModeSymlink) == os.ModeSymlink } // Copy data from src to dst // // If followSymlinks is not set and src is a symbolic link, a // new symlink will be created instead of copying the file it points // to. func CopyFile(src, dst string, followSymlinks bool) error { if samefile(src, dst) { return &SameFileError{src, dst} } // Make sure src exists and neither are special files srcStat, err := os.Lstat(src) if err != nil { return err } if specialfile(srcStat) { return &SpecialFileError{src, srcStat} } dstStat, err := os.Stat(dst) if err != nil && !os.IsNotExist(err) { return err } else if err == nil { if specialfile(dstStat) { return &SpecialFileError{dst, dstStat} } } // If we don't follow symlinks and it's a symlink, just link it and be done if !followSymlinks && IsSymlink(srcStat) { return os.Symlink(src, dst) } // If we are a symlink, follow it if IsSymlink(srcStat) { src, err = os.Readlink(src) if err != nil { return err } srcStat, err = os.Stat(src) if err != nil { return err } } // Do the actual copy fsrc, err := os.Open(src) if err != nil { return err } defer fsrc.Close() fdst, err := os.Create(dst) if err != nil { return err } defer fdst.Close() size, err := io.Copy(fdst, fsrc) if err != nil { return err } if size != srcStat.Size() { return fmt.Errorf("%s: %d/%d copied", src, size, srcStat.Size()) } return nil } // Copy mode bits from src to dst. // // If followSymlinks is false, symlinks aren't followed if and only // if both `src` and `dst` are symlinks. If `lchmod` isn't available // and both are symlinks this does nothing. (I don't think lchmod is // available in Go) func CopyMode(src, dst string, followSymlinks bool) error { srcStat, err := os.Lstat(src) if err != nil { return err } dstStat, err := os.Lstat(dst) if err != nil { return err } // They are both symlinks and we can't change mode on symlinks. if !followSymlinks && IsSymlink(srcStat) && IsSymlink(dstStat) { return nil } // Atleast one is not a symlink, get the actual file stats srcStat, _ = os.Stat(src) err = os.Chmod(dst, srcStat.Mode()) return err } // Copy data and mode bits ("cp src dst"). Return the file's destination. // // The destination may be a directory. // // If followSymlinks is false, symlinks won't be followed. This // resembles GNU's "cp -P src dst". // // If source and destination are the same file, a SameFileError will be // rased. func Copy(src, dst string, followSymlinks bool) (string, error) { dstInfo, err := os.Stat(dst) if err == nil && dstInfo.Mode().IsDir() { dst = filepath.Join(dst, filepath.Base(src)) } if err != nil && !os.IsNotExist(err) { return dst, err } err = CopyFile(src, dst, followSymlinks) if err != nil { return dst, err } err = CopyMode(src, dst, followSymlinks) if err != nil { return dst, err } return dst, nil } type CopyTreeOptions struct { Symlinks bool IgnoreDanglingSymlinks bool CopyFunction func(string, string, bool) (string, error) Ignore func(string, []os.FileInfo) []string } // Recursively copy a directory tree. // // The destination directory must not already exist. // // If the optional Symlinks flag is true, symbolic links in the // source tree result in symbolic links in the destination tree; if // it is false, the contents of the files pointed to by symbolic // links are copied. If the file pointed by the symlink doesn't // exist, an error will be returned. // // You can set the optional IgnoreDanglingSymlinks flag to true if you // want to silence this error. Notice that this has no effect on // platforms that don't support os.Symlink. // // The optional ignore argument is a callable. If given, it // is called with the `src` parameter, which is the directory // being visited by CopyTree(), and `names` which is the list of // `src` contents, as returned by ioutil.ReadDir(): // // callable(src, entries) -> ignoredNames // // Since CopyTree() is called recursively, the callable will be // called once for each directory that is copied. It returns a // list of names relative to the `src` directory that should // not be copied. // // The optional copyFunction argument is a callable that will be used // to copy each file. It will be called with the source path and the // destination path as arguments. By default, Copy() is used, but any // function that supports the same signature (like Copy2() when it // exists) can be used. func CopyTree(src, dst string, options *CopyTreeOptions) error { if options == nil { options = &CopyTreeOptions{Symlinks: false, Ignore: nil, CopyFunction: Copy, IgnoreDanglingSymlinks: false} } srcFileInfo, err := os.Stat(src) if err != nil { return err } if !srcFileInfo.IsDir() { return &NotADirectoryError{src} } _, err = os.Open(dst) if !os.IsNotExist(err) { return &AlreadyExistsError{dst} } entries, err := ioutil.ReadDir(src) if err != nil { return err } err = os.MkdirAll(dst, srcFileInfo.Mode()) if err != nil { return err } ignoredNames := []string{} if options.Ignore != nil { ignoredNames = options.Ignore(src, entries) } for _, entry := range entries { if stringInSlice(entry.Name(), ignoredNames) { continue } srcPath := filepath.Join(src, entry.Name()) dstPath := filepath.Join(dst, entry.Name()) entryFileInfo, err := os.Lstat(srcPath) if err != nil { return err } // Deal with symlinks if IsSymlink(entryFileInfo) { linkTo, err := os.Readlink(srcPath) if err != nil { return err } if options.Symlinks { os.Symlink(linkTo, dstPath) //CopyStat(srcPath, dstPath, false) } else { // ignore dangling symlink if flag is on _, err = os.Stat(linkTo) if os.IsNotExist(err) && options.IgnoreDanglingSymlinks { continue } _, err = options.CopyFunction(srcPath, dstPath, false) if err != nil { return err } } } else if entryFileInfo.IsDir() { err = CopyTree(srcPath, dstPath, options) if err != nil { return err } } else { _, err = options.CopyFunction(srcPath, dstPath, false) if err != nil { return err } } } return nil }