yos/common.go
package yos
import (
"errors"
"os"
"path/filepath"
"regexp"
"syscall"
"github.com/1set/gut/ystring"
)
var (
errInvalidPath = errors.New("invalid path")
errSameFile = errors.New("files are identical")
errShortRead = errors.New("short read")
errIsDirectory = errors.New("is a directory")
errNotDirectory = errors.New("not a directory")
errNotRegularFile = errors.New("not a regular file")
errNotSymlink = errors.New("not a symbolic link")
errStepOutDir = errors.New("yos: step out this directory")
)
// operation names for the Op field of os.PathError.
var (
opnCompare = "compare"
opnCopy = "copy"
opnMove = "move"
opnList = "list"
opnSize = "size"
opnEmpty = "empty"
opnChange = "change"
opnMake = "make"
)
// internal use
var (
emptyStr = ""
)
// underlyingError returns the underlying error for known os error types. forked from: os/error.go
func underlyingError(err error) error {
switch err := err.(type) {
case *os.LinkError:
return err.Err
case *os.PathError:
return err.Err
case *os.SyscallError:
return err.Err
}
return err
}
// opError returns error struct with given details.
func opError(op, path string, err error) *os.PathError {
return &os.PathError{
Op: op,
Path: path,
Err: underlyingError(err),
}
}
type (
funcStatFileInfo func(name string) (os.FileInfo, error)
funcCheckFileInfo func(fi *os.FileInfo) bool
funcRemoveEntry func(path string) error
funcCopyEntry func(src, dest string) error
)
// isFileFi indicates whether the FileInfo is for a regular file.
func isFileFi(fi *os.FileInfo) bool {
return fi != nil && (*fi).Mode().IsRegular()
}
// isDirFi indicates whether the FileInfo is for a directory.
func isDirFi(fi *os.FileInfo) bool {
return fi != nil && (*fi).Mode().IsDir()
}
// isSymlinkFi indicates whether the FileInfo is for a symbolic link.
func isSymlinkFi(fi *os.FileInfo) bool {
return fi != nil && ((*fi).Mode()&os.ModeType == os.ModeSymlink)
}
func isLinkErrorCrossDevice(err error) bool {
lerr, ok := err.(*os.LinkError)
return ok && lerr.Err == syscall.EXDEV
}
func isLinkErrorNotDirectory(err error) bool {
lerr, ok := err.(*os.LinkError)
return ok && lerr.Err == syscall.ENOTDIR
}
// refineOpPaths validates, cleans up and adjusts the source and destination paths for operations like copy or move.
func refineOpPaths(opName, srcRaw, destRaw string, followLink bool) (src, dest string, err error) {
// validate paths, and quit if got error
if ystring.IsBlank(srcRaw) {
err = opError(opName, srcRaw, errInvalidPath)
} else if ystring.IsBlank(destRaw) {
err = opError(opName, destRaw, errInvalidPath)
}
if err != nil {
return
}
// clean up paths
src, dest = filepath.Clean(srcRaw), filepath.Clean(destRaw)
// use os.Stat to follow symbolic links
statFunc := os.Lstat
if followLink {
statFunc = os.Stat
}
// check if source exists
var srcInfo, destInfo os.FileInfo
if srcInfo, err = statFunc(src); err != nil {
return
}
// check if destination exists
if destInfo, err = statFunc(dest); err != nil {
// check existence of parent of the missing destination
if os.IsNotExist(err) {
_, err = os.Stat(filepath.Dir(dest))
}
} else {
if os.SameFile(srcInfo, destInfo) {
err = opError(opName, dest, errSameFile)
} else if destInfo.IsDir() {
// append file name of source to path of the existing destination
dest = JoinPath(dest, srcInfo.Name())
}
}
return
}
// refineComparePaths validates, cleans up path for file comparison.
func refineComparePaths(pathRaw1, pathRaw2 string) (path1, path2 string, err error) {
// validate paths
if ystring.IsBlank(pathRaw1) {
err = opError(opnCompare, pathRaw1, errInvalidPath)
} else if ystring.IsBlank(pathRaw2) {
err = opError(opnCompare, pathRaw2, errInvalidPath)
}
// clean up paths
if err == nil {
path1, path2 = filepath.Clean(pathRaw1), filepath.Clean(pathRaw2)
}
return
}
// resolveDirInfo returns file info of a path if it's a directory or a symbolic link to a directory, otherwise returns an error.
func resolveDirInfo(pathRaw string) (path string, fi os.FileInfo, err error) {
if fi, err = os.Lstat(pathRaw); err == nil {
// resolve to real path if the given path is a symbolic link
if isSymlinkFi(&fi) {
if path, err = filepath.EvalSymlinks(pathRaw); err == nil {
// update file info for the real path
fi, err = os.Lstat(path)
}
if err != nil {
path = emptyStr
return
}
} else {
// simply clean the path if the raw path isn't a symbolic link to resolve
path = filepath.Clean(pathRaw)
}
// check if the final path is a directory
if !isDirFi(&fi) {
err, path = errNotDirectory, emptyStr
}
}
return
}
// openFileInfo returns file descriptor and info of a path if it's a regular file, otherwise returns an error.
func openFileInfo(path string) (file *os.File, fi os.FileInfo, err error) {
if fi, err = os.Stat(path); err == nil {
if isFileFi(&fi) {
if file, err = os.Open(path); err == nil {
return
}
} else {
err = errNotRegularFile
}
}
return
}
// compileRegexpList compiles a list of string patterns into regexp patterns.
func compileRegexpList(strPats []string) (rePats []*regexp.Regexp, err error) {
var rp *regexp.Regexp
rePats = make([]*regexp.Regexp, 0, len(strPats))
for _, sp := range strPats {
if rp, err = regexp.Compile(sp); err == nil {
rePats = append(rePats, rp)
} else {
err = opError(opnList, sp, err)
break
}
}
return
}