/* * This file is part of PathsHelper library. * * Copyright 2018 Arduino AG (http://www.arduino.cc/) * * PathsHelper library is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA * * As a special exception, you may use this file as part of a free software * library without restriction. Specifically, if other files instantiate * templates or use macros or inline functions from this file, or you compile * this file and link it with other files to produce an executable, this * file does not by itself cause the resulting executable to be covered by * the GNU General Public License. This exception does not however * invalidate any other reasons why the executable file might be covered by * the GNU General Public License. */ package paths import ( "fmt" "io" "io/fs" "os" "path/filepath" "strings" "syscall" "time" ) // Path represents a path type Path struct { path string } // New creates a new Path object. If path is the empty string // then nil is returned. func New(path ...string) *Path { if len(path) == 0 { return nil } if len(path) == 1 && path[0] == "" { return nil } res := &Path{path: path[0]} if len(path) > 1 { return res.Join(path[1:]...) } return res } // NewFromFile creates a new Path object using the path name // obtained from the File object (see os.File.Name function). func NewFromFile(file *os.File) *Path { return New(file.Name()) } // Stat returns a FileInfo describing the named file. The result is // cached internally for next queries. To ensure that the cached // FileInfo entry is updated just call Stat again. func (p *Path) Stat() (fs.FileInfo, error) { return os.Stat(p.path) } // Lstat returns a FileInfo describing the named file. If the file is // a symbolic link, the returned FileInfo describes the symbolic link. // Lstat makes no attempt to follow the link. If there is an error, it // will be of type *PathError. func (p *Path) Lstat() (fs.FileInfo, error) { return os.Lstat(p.path) } // Clone create a copy of the Path object func (p *Path) Clone() *Path { return New(p.path) } // Join create a new Path by joining the provided paths func (p *Path) Join(paths ...string) *Path { return New(filepath.Join(p.path, filepath.Join(paths...))) } // JoinPath create a new Path by joining the provided paths func (p *Path) JoinPath(paths ...*Path) *Path { res := p.Clone() for _, path := range paths { res = res.Join(path.path) } return res } // Base returns the last element of path func (p *Path) Base() string { return filepath.Base(p.path) } // Ext returns the file name extension used by path func (p *Path) Ext() string { return filepath.Ext(p.path) } // HasPrefix returns true if the file name has one of the // given prefixes (the Base() method is used to obtain the // file name used for the comparison) func (p *Path) HasPrefix(prefixes ...string) bool { filename := p.Base() for _, prefix := range prefixes { if strings.HasPrefix(filename, prefix) { return true } } return false } // HasSuffix returns true if the file name has one of the // given suffixies func (p *Path) HasSuffix(suffixies ...string) bool { filename := p.String() for _, suffix := range suffixies { if strings.HasSuffix(filename, suffix) { return true } } return false } // RelTo returns a relative Path that is lexically equivalent to r when // joined to the current Path. // // For example paths.New("/my/path/ab/cd").RelTo(paths.New("/my/path")) // returns "../..". func (p *Path) RelTo(r *Path) (*Path, error) { rel, err := filepath.Rel(p.path, r.path) if err != nil { return nil, err } return New(rel), nil } // RelFrom returns a relative Path that when joined with r is lexically // equivalent to the current path. // // For example paths.New("/my/path/ab/cd").RelFrom(paths.New("/my/path")) // returns "ab/cd". func (p *Path) RelFrom(r *Path) (*Path, error) { rel, err := filepath.Rel(r.path, p.path) if err != nil { return nil, err } return New(rel), nil } // Abs returns the absolute path of the current Path func (p *Path) Abs() (*Path, error) { abs, err := filepath.Abs(p.path) if err != nil { return nil, err } return New(abs), nil } // IsAbs returns true if the Path is absolute func (p *Path) IsAbs() bool { return filepath.IsAbs(p.path) } // ToAbs transofrm the current Path to the corresponding absolute path func (p *Path) ToAbs() error { abs, err := filepath.Abs(p.path) if err != nil { return err } p.path = abs return nil } // Clean Clean returns the shortest path name equivalent to path by // purely lexical processing func (p *Path) Clean() *Path { return New(filepath.Clean(p.path)) } // IsInsideDir returns true if the current path is inside the provided // dir func (p *Path) IsInsideDir(dir *Path) (bool, error) { rel, err := filepath.Rel(dir.path, p.path) if err != nil { // If the dir cannot be made relative to this path it means // that it belong to a different filesystems, so it cannot be // inside this path. return false, nil } return !strings.Contains(rel, ".."+string(os.PathSeparator)) && rel != ".." && rel != ".", nil } // Parent returns all but the last element of path, typically the path's // directory or the parent directory if the path is already a directory func (p *Path) Parent() *Path { return New(filepath.Dir(p.path)) } // Mkdir create a directory denoted by the current path func (p *Path) Mkdir() error { return os.Mkdir(p.path, 0755) } // MkdirAll creates a directory named path, along with any necessary // parents, and returns nil, or else returns an error func (p *Path) MkdirAll() error { return os.MkdirAll(p.path, os.FileMode(0755)) } // Remove removes the named file or directory func (p *Path) Remove() error { return os.Remove(p.path) } // RemoveAll removes path and any children it contains. It removes // everything it can but returns the first error it encounters. If // the path does not exist, RemoveAll returns nil (no error). func (p *Path) RemoveAll() error { return os.RemoveAll(p.path) } // Rename renames (moves) the path to newpath. If newpath already exists // and is not a directory, Rename replaces it. OS-specific restrictions // may apply when oldpath and newpath are in different directories. If // there is an error, it will be of type *os.LinkError. func (p *Path) Rename(newpath *Path) error { return os.Rename(p.path, newpath.path) } // MkTempDir creates a new temporary directory inside the path // pointed by the Path object with a name beginning with prefix // and returns the path of the new directory. func (p *Path) MkTempDir(prefix string) (*Path, error) { return MkTempDir(p.path, prefix) } // FollowSymLink transforms the current path to the path pointed by the // symlink if path is a symlink, otherwise it does nothing func (p *Path) FollowSymLink() error { resolvedPath, err := filepath.EvalSymlinks(p.path) if err != nil { return err } p.path = resolvedPath return nil } // Exist return true if the file denoted by this path exists, false // in any other case (also in case of error). func (p *Path) Exist() bool { exist, err := p.ExistCheck() return exist && err == nil } // NotExist return true if the file denoted by this path DO NOT exists, false // in any other case (also in case of error). func (p *Path) NotExist() bool { exist, err := p.ExistCheck() return !exist && err == nil } // ExistCheck return true if the path exists or false if the path doesn't exists. // In case the check fails false is returned together with the corresponding error. func (p *Path) ExistCheck() (bool, error) { _, err := p.Stat() if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } if err.(*os.PathError).Err == syscall.ENOTDIR { return false, nil } return false, err } // IsDir returns true if the path exists and is a directory. In all the other // cases (and also in case of any error) false is returned. func (p *Path) IsDir() bool { isdir, err := p.IsDirCheck() return isdir && err == nil } // IsNotDir returns true if the path exists and is NOT a directory. In all the other // cases (and also in case of any error) false is returned. func (p *Path) IsNotDir() bool { isdir, err := p.IsDirCheck() return !isdir && err == nil } // IsDirCheck return true if the path exists and is a directory or false // if the path exists and is not a directory. In all the other case false and // the corresponding error is returned. func (p *Path) IsDirCheck() (bool, error) { info, err := p.Stat() if err == nil { return info.IsDir(), nil } return false, err } // CopyTo 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 it's contents will be replaced by the contents // of the source file. The file mode will be copied from the source and // the copied data is synced/flushed to stable storage. func (p *Path) CopyTo(dst *Path) error { if p.EqualsTo(dst) { return fmt.Errorf("%s and %s are the same file", p.path, dst.path) } in, err := os.Open(p.path) if err != nil { return err } defer in.Close() out, err := os.Create(dst.path) if err != nil { return err } defer out.Close() if _, err := io.Copy(out, in); err != nil { return err } if err := out.Sync(); err != nil { return err } si, err := p.Stat() if err != nil { return err } err = os.Chmod(dst.path, si.Mode()) if err != nil { return err } return nil } // CopyDirTo recursively copies the directory denoted by the current path to // the destination path. The source directory must exist and the destination // directory must NOT exist (no implicit destination name allowed). // Symlinks are not copied, they will be supported in future versions. func (p *Path) CopyDirTo(dst *Path) error { src := p.Clean() dst = dst.Clean() srcFiles, err := src.ReadDir() if err != nil { return fmt.Errorf("error reading source dir %s: %s", src, err) } if exist, err := dst.ExistCheck(); exist { return fmt.Errorf("destination %s already exists", dst) } else if err != nil { return fmt.Errorf("checking if %s exists: %s", dst, err) } if err := dst.MkdirAll(); err != nil { return fmt.Errorf("creating destination dir %s: %s", dst, err) } srcInfo, err := src.Stat() if err != nil { return fmt.Errorf("getting stat info for %s: %s", src, err) } if err := os.Chmod(dst.path, srcInfo.Mode()); err != nil { return fmt.Errorf("setting permission for dir %s: %s", dst, err) } for _, srcPath := range srcFiles { srcPathInfo, err := srcPath.Stat() if err != nil { return fmt.Errorf("getting stat info for %s: %s", srcPath, err) } dstPath := dst.Join(srcPath.Base()) if srcPathInfo.IsDir() { if err := srcPath.CopyDirTo(dstPath); err != nil { return fmt.Errorf("copying %s to %s: %s", srcPath, dstPath, err) } continue } // Skip symlinks. if srcPathInfo.Mode()&os.ModeSymlink != 0 { // TODO continue } if err := srcPath.CopyTo(dstPath); err != nil { return fmt.Errorf("copying %s to %s: %s", srcPath, dstPath, err) } } return nil } // Chmod changes the mode of the named file to mode. If the file is a // symbolic link, it changes the mode of the link's target. If there // is an error, it will be of type *os.PathError. func (p *Path) Chmod(mode fs.FileMode) error { return os.Chmod(p.path, mode) } // Chtimes changes the access and modification times of the named file, // similar to the Unix utime() or utimes() functions. func (p *Path) Chtimes(atime, mtime time.Time) error { return os.Chtimes(p.path, atime, mtime) } // ReadFile reads the file named by filename and returns the contents func (p *Path) ReadFile() ([]byte, error) { return os.ReadFile(p.path) } // WriteFile writes data to a file named by filename. If the file // does not exist, WriteFile creates it otherwise WriteFile truncates // it before writing. func (p *Path) WriteFile(data []byte) error { return os.WriteFile(p.path, data, os.FileMode(0644)) } // WriteToTempFile writes data to a newly generated temporary file. // dir and prefix have the same meaning for MkTempFile. // In case of success the Path to the temp file is returned. func WriteToTempFile(data []byte, dir *Path, prefix string) (res *Path, err error) { f, err := MkTempFile(dir, prefix) if err != nil { return nil, err } defer f.Close() if n, err := f.Write(data); err != nil { return nil, err } else if n < len(data) { return nil, fmt.Errorf("could not write all data (written %d bytes out of %d)", n, len(data)) } return New(f.Name()), nil } // ReadFileAsLines reads the file named by filename and returns it as an // array of lines. This function takes care of the newline encoding // differences between different OS func (p *Path) ReadFileAsLines() ([]string, error) { data, err := p.ReadFile() if err != nil { return nil, err } txt := string(data) txt = strings.Replace(txt, "\r\n", "\n", -1) return strings.Split(txt, "\n"), nil } // Truncate create an empty file named by path or if the file already // exist it truncates it (delete all contents) func (p *Path) Truncate() error { return p.WriteFile([]byte{}) } // Open opens a file for reading. It calls os.Open on the // underlying path. func (p *Path) Open() (*os.File, error) { return os.Open(p.path) } // Create creates or truncates a file. It calls os.Create // on the underlying path. func (p *Path) Create() (*os.File, error) { return os.Create(p.path) } // Append opens a file for append or creates it if the file doesn't exist. func (p *Path) Append() (*os.File, error) { return os.OpenFile(p.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666) } // EqualsTo return true if both paths are equal func (p *Path) EqualsTo(other *Path) bool { return p.path == other.path } // EquivalentTo return true if both paths are equivalent (they points to the // same file even if they are lexicographically different) based on the current // working directory. func (p *Path) EquivalentTo(other *Path) bool { if p.Clean().path == other.Clean().path { return true } if infoP, err := p.Stat(); err != nil { // go ahead with the next test... } else if infoOther, err := other.Stat(); err != nil { // go ahead with the next test... } else if os.SameFile(infoP, infoOther) { return true } if absP, err := p.Abs(); err != nil { return false } else if absOther, err := other.Abs(); err != nil { return false } else { return absP.path == absOther.path } } // Parents returns all the parents directories of the current path. If the path is absolute // it starts from the current path to the root, if the path is relative is starts from the // current path to the current directory. // The path should be clean for this method to work properly (no .. or . or other shortcuts). // This function does not performs any check on the returned paths. func (p *Path) Parents() []*Path { res := []*Path{} dir := p for { res = append(res, dir) parent := dir.Parent() if parent.EquivalentTo(dir) { break } dir = parent } return res } func (p *Path) String() string { return p.path } // Canonical return a "canonical" Path for the given filename. // The meaning of "canonical" is OS-dependent but the goal of this method // is to always return the same path for a given file (factoring out all the // possibile ambiguities including, for example, relative paths traversal, // symlinks, drive volume letter case, etc). func (p *Path) Canonical() *Path { canonical := p.Clone() // https://github.com/golang/go/issues/17084#issuecomment-246645354 canonical.FollowSymLink() if absPath, err := canonical.Abs(); err == nil { canonical = absPath } return canonical }