2022-06-28 13:49:32 +08:00
|
|
|
|
package localfile
|
|
|
|
|
|
|
|
|
|
import (
|
2022-06-28 15:10:54 +08:00
|
|
|
|
"fmt"
|
2022-06-28 13:49:32 +08:00
|
|
|
|
"io/fs"
|
|
|
|
|
"io/ioutil"
|
|
|
|
|
"os"
|
|
|
|
|
"path"
|
|
|
|
|
"path/filepath"
|
|
|
|
|
"strings"
|
|
|
|
|
)
|
|
|
|
|
|
2022-07-02 07:27:49 +08:00
|
|
|
|
// SymlinkFile 软链接文件,Linux/macOS的ln,Windows的mklink命令创建的文件链接。对于非软链接文件而言,真实的路径和逻辑路径是一样的。
|
2022-06-28 13:49:32 +08:00
|
|
|
|
type SymlinkFile struct {
|
|
|
|
|
// LogicPath 逻辑路径
|
|
|
|
|
LogicPath string `json:"logicPath"`
|
|
|
|
|
// RealPath 真正的文件路径,即文件的本体
|
|
|
|
|
RealPath string `json:"realPath"`
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-28 15:10:54 +08:00
|
|
|
|
func (s *SymlinkFile) String() string {
|
|
|
|
|
sb := &strings.Builder{}
|
|
|
|
|
fmt.Fprintf(sb, "{\"logicPath\":%s, \"realPath\": %s}", s.LogicPath, s.RealPath)
|
|
|
|
|
return sb.String()
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-28 13:49:32 +08:00
|
|
|
|
func NewSymlinkFile(filePath string) SymlinkFile {
|
2022-07-03 10:32:39 +08:00
|
|
|
|
p := CleanPath(filePath)
|
2022-06-28 13:49:32 +08:00
|
|
|
|
return SymlinkFile{
|
|
|
|
|
LogicPath: p,
|
|
|
|
|
RealPath: p,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type MyWalkFunc func(path SymlinkFile, info fs.FileInfo, err error) error
|
|
|
|
|
|
2022-07-03 10:32:39 +08:00
|
|
|
|
// CleanPath 规范化文件路径,分隔符全部转换成Unix文件分隔符"/"。同时清除后缀无用的"/"路径
|
|
|
|
|
func CleanPath(p string) string {
|
|
|
|
|
if p == "" || p == "/" {
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
p = path.Clean(strings.ReplaceAll(p, "\\", "/"))
|
|
|
|
|
if p == "." {
|
|
|
|
|
p = ""
|
|
|
|
|
}
|
|
|
|
|
return p
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetSuffixPath 获取相对路径。即fullFilePath相对rootFilePath的相对路径
|
|
|
|
|
func GetSuffixPath(fullFilePath, rootFilePath string) string {
|
|
|
|
|
rootFilePath = CleanPath(rootFilePath)
|
|
|
|
|
fullFilePath = CleanPath(fullFilePath)
|
|
|
|
|
return strings.TrimPrefix(strings.TrimPrefix(fullFilePath, rootFilePath), "/")
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-28 13:49:32 +08:00
|
|
|
|
// RetrieveRealPath 递归调用找到软链接文件的真实文件对应的路径信息
|
|
|
|
|
func RetrieveRealPath(file SymlinkFile) (SymlinkFile, os.FileInfo, error) {
|
|
|
|
|
info, err := os.Lstat(file.RealPath)
|
|
|
|
|
if err == nil {
|
|
|
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
|
|
|
// 软链接文件
|
|
|
|
|
if f, e := os.Readlink(file.RealPath); e == nil {
|
|
|
|
|
file.RealPath = strings.ReplaceAll(f, "\\", "/")
|
|
|
|
|
return RetrieveRealPath(file)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return file, info, err
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-03 07:45:40 +08:00
|
|
|
|
// RetrieveRealPathFromLogicPath 遍历路径获取逻辑路径真正的文件路径。如果逻辑路径不完全存在,则返回已经存在的那部分路径
|
|
|
|
|
//
|
|
|
|
|
// logicFilePath - 目标逻辑路径
|
|
|
|
|
// 由于目标逻辑路径期间有可能会经过多次符号逻辑文件,同时有部分逻辑路径可能是不存在的,所以需要按照逻辑文件起始开始进行遍历,直至完成逻辑文件路径的所有遍历,
|
|
|
|
|
// 或者直到不存在的逻辑文件部分,然后返回。
|
|
|
|
|
//
|
|
|
|
|
// 例如逻辑文件路径:/Volumes/Downloads/dev/sync_drive_config.json。
|
|
|
|
|
//
|
|
|
|
|
// 如果/Volumes/Downloads存在而后面部分不存在,则最终结果返回/Volumes/Downloads对应的文件信息,同时返回error
|
|
|
|
|
func RetrieveRealPathFromLogicPath(logicFilePath string) (SymlinkFile, os.FileInfo, error) {
|
2022-07-03 10:32:39 +08:00
|
|
|
|
logicFilePath = CleanPath(logicFilePath)
|
2022-07-03 07:45:40 +08:00
|
|
|
|
if logicFilePath == "/" {
|
|
|
|
|
return RetrieveRealPath(NewSymlinkFile(logicFilePath))
|
|
|
|
|
}
|
|
|
|
|
pathParts := strings.Split(logicFilePath, "/")
|
|
|
|
|
exitedSymlinkFile := NewSymlinkFile("")
|
|
|
|
|
var exitedFileInfo os.FileInfo
|
|
|
|
|
|
|
|
|
|
sf := NewSymlinkFile("")
|
|
|
|
|
var fi os.FileInfo
|
|
|
|
|
var err error
|
|
|
|
|
for _, p := range pathParts {
|
|
|
|
|
if p == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(p, ":") {
|
|
|
|
|
// windows volume label, e.g: C:/ D:/
|
|
|
|
|
sf.LogicPath += p
|
|
|
|
|
sf.RealPath += p
|
|
|
|
|
exitedSymlinkFile = sf
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
sf.LogicPath += "/" + p
|
|
|
|
|
sf.RealPath += "/" + p
|
|
|
|
|
sf, fi, err = RetrieveRealPath(sf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// may be permission deny or not existed
|
|
|
|
|
return exitedSymlinkFile, exitedFileInfo, err
|
|
|
|
|
}
|
|
|
|
|
exitedSymlinkFile = sf
|
|
|
|
|
exitedFileInfo = fi
|
|
|
|
|
}
|
|
|
|
|
return exitedSymlinkFile, exitedFileInfo, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-03 10:32:39 +08:00
|
|
|
|
// RetrieveRealPathFromLogicSuffixPath 通过指定根目录和后缀逻辑路径,获取文件对应的真实路径信息
|
|
|
|
|
//
|
|
|
|
|
// 整体功能和 RetrieveRealPathFromLogicPath 一致,将逻辑路径分成根路径和后缀路径两部分,并且根路径必须存在。
|
|
|
|
|
func RetrieveRealPathFromLogicSuffixPath(rootFile SymlinkFile, logicSuffixPath string) (SymlinkFile, os.FileInfo, error) {
|
|
|
|
|
if rootFile.LogicPath == "" || rootFile.RealPath == "" {
|
|
|
|
|
return rootFile, nil, fmt.Errorf("root file path error")
|
|
|
|
|
}
|
|
|
|
|
rootFile.LogicPath = CleanPath(rootFile.LogicPath)
|
|
|
|
|
rootFile.RealPath = CleanPath(rootFile.RealPath)
|
|
|
|
|
rootFileInfo, e := os.Lstat(rootFile.RealPath)
|
|
|
|
|
if e != nil {
|
|
|
|
|
return rootFile, nil, e
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logicSuffixPath = CleanPath(logicSuffixPath)
|
|
|
|
|
pathParts := strings.Split(logicSuffixPath, "/")
|
|
|
|
|
exitedSymlinkFile := rootFile
|
|
|
|
|
exitedFileInfo := rootFileInfo
|
|
|
|
|
|
|
|
|
|
sf := rootFile
|
|
|
|
|
var fi os.FileInfo
|
|
|
|
|
var err error
|
|
|
|
|
for _, p := range pathParts {
|
|
|
|
|
if p == "" {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
sf.LogicPath += "/" + p
|
|
|
|
|
sf.RealPath += "/" + p
|
|
|
|
|
sf, fi, err = RetrieveRealPath(sf)
|
|
|
|
|
if err != nil {
|
|
|
|
|
// may be permission deny or not existed
|
|
|
|
|
return exitedSymlinkFile, exitedFileInfo, err
|
|
|
|
|
}
|
|
|
|
|
exitedSymlinkFile = sf
|
|
|
|
|
exitedFileInfo = fi
|
|
|
|
|
}
|
|
|
|
|
return exitedSymlinkFile, exitedFileInfo, nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-03 07:45:40 +08:00
|
|
|
|
// WalkAllFile 遍历本地文件,支持软链接(符号逻辑)文件(Linux & Windows & macOS)
|
2022-06-28 13:49:32 +08:00
|
|
|
|
func WalkAllFile(file SymlinkFile, walkFn MyWalkFunc) error {
|
|
|
|
|
file.LogicPath = path.Clean(strings.ReplaceAll(file.LogicPath, "\\", "/"))
|
|
|
|
|
file.RealPath = path.Clean(strings.ReplaceAll(file.RealPath, "\\", "/"))
|
|
|
|
|
|
|
|
|
|
file, info, err := RetrieveRealPath(file)
|
|
|
|
|
if err != nil {
|
|
|
|
|
err = walkFn(file, nil, err)
|
|
|
|
|
} else {
|
|
|
|
|
err = walkAllFile(file, info, walkFn)
|
|
|
|
|
}
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func walkAllFile(file SymlinkFile, info os.FileInfo, walkFn MyWalkFunc) error {
|
|
|
|
|
if !info.IsDir() {
|
|
|
|
|
return walkFn(file, info, nil)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
files, err1 := ioutil.ReadDir(file.RealPath)
|
|
|
|
|
if err1 != nil {
|
|
|
|
|
return walkFn(file, nil, err1)
|
|
|
|
|
}
|
|
|
|
|
for _, fi := range files {
|
|
|
|
|
subFile := SymlinkFile{
|
|
|
|
|
LogicPath: path.Join(path.Clean(file.LogicPath), "/", fi.Name()),
|
|
|
|
|
RealPath: path.Join(path.Clean(file.RealPath), "/", fi.Name()),
|
|
|
|
|
}
|
|
|
|
|
subFile, fi, err1 = RetrieveRealPath(subFile)
|
|
|
|
|
err := walkFn(subFile, fi, err1)
|
|
|
|
|
if err != nil && err != filepath.SkipDir {
|
|
|
|
|
return err
|
|
|
|
|
}
|
2022-07-02 16:24:11 +08:00
|
|
|
|
if fi == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2022-06-28 13:49:32 +08:00
|
|
|
|
if fi.IsDir() {
|
|
|
|
|
if err == filepath.SkipDir {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
err = walkAllFile(subFile, fi, walkFn)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return err
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|