Brad Peabody
Brad Peabody

Reputation: 11397

Determine if path is inside another path in Go

I would like to delete all path components for a file up to (but not including) an overall base directory.

Example: /overall/basedir/a/b/c/file

I want to remove "file" and then remove "c", "b" and then "a" if possible (directories not empty). I do not want to unlink "basedir" or "overall".

filepath.HasPrefix would seem to be a good option but it's apparently deprecated: https://golang.org/pkg/path/filepath/#HasPrefix

What I have now is:

p := THEPATH

// attempt to remove file and all parent directories up to the basedir
// FIXME: HasPrefix is apparently bad.. a better idea?
for filepath.HasPrefix(p, baseDir) {
    err := os.Remove(p)
    if err != nil {
        break
    }
    // climb up one
    p = filepath.Dir(p)
}

Looking for a succinct and reliable way that works on all Go supported platforms.

Upvotes: 0

Views: 2429

Answers (1)

putu
putu

Reputation: 6454

IMHO, path handling is rather complicated if you want to support all platforms that is supported by golang. Bellow is the solution that I've implemented so far (probably not the simplest one). Notes:

  1. It supports generalized action rather than only os.Remove
  2. Instead of string-based path comparison, function os.SameFile is used to test whether two files/directories are equal.
  3. In the implementation, at first all candidate paths are visited and added to visitedPaths slice. Then, if no error occurs, an action is perform to each candidate path.

The code:

package pathwalker

import (
    "os"
    "path/filepath"
    "strings"
)

type PathAction func(PathInfo) error
type PathInfo struct {
    FileInfo os.FileInfo
    FullPath string
}
type PathWalker struct {
    pathName     string
    basePath     string
    visitedPaths []PathInfo
    lastFi       os.FileInfo
}

//NewPathWalker creates PathWalker instance
func NewPathWalker(pathName, basePath string) *PathWalker {
    return &PathWalker{
        pathName: pathName,
        basePath: basePath,
    }
}

func (w *PathWalker) visit() (bool, error) {
    //Make sure path ends with separator
    basePath := filepath.Clean(w.basePath + string(filepath.Separator))
    baseInfo, err := os.Lstat(basePath)
    if err != nil {
        return false, err
    }

    //clean path name
    fi, err := os.Lstat(w.pathName)
    if err != nil {
        return false, err
    } else if fi.IsDir() {
        //When pathname is a directory, remove latest separator
        sep := string(filepath.Separator)
        cleanPath := filepath.Clean(w.pathName + sep)
        w.pathName = strings.TrimRight(cleanPath, sep)
    } else {
        w.pathName = filepath.Clean(w.pathName)
    }
    return w.doVisit(w.pathName, baseInfo)
}

//visit path recursively
func (w *PathWalker) doVisit(pathName string, baseInfo os.FileInfo) (bool, error) {
    //Get file info
    fi, err := os.Lstat(pathName)
    if err != nil {
        return false, err
    }

    //Stop when basePath equal to pathName
    if os.SameFile(fi, baseInfo) {
        return true, nil
    }

    //Top directory reached, but does not match baseInfo
    if w.lastFi != nil && os.SameFile(w.lastFi, fi) {
        return false, nil
    }
    w.lastFi = fi

    //Append to visited path list
    w.visitedPaths = append(w.visitedPaths, PathInfo{fi, pathName})

    //Move to upper path
    up := filepath.Dir(pathName)
    if up == "." {
        return false, nil
    }

    //Visit upper directory
    return w.doVisit(up, baseInfo)
}

//Walk perform action then return number of proceed paths and error
func (w *PathWalker) Walk(act PathAction) (int, error) {
    n := 0
    ok, err := w.visit()
    if err != nil {
        return 0, err
    } else if ok && act != nil {
        for _, pi := range w.visitedPaths {
            err := act(pi)
            if err != nil {
                return n, err
            }
            n++
        }
    }
    return n, nil
}

//VisitedPaths return list of visited paths
func (w *PathWalker) VisitedPaths() []PathInfo {
    return w.visitedPaths
}

Then if you want to remove file and parent directory under basePath, you can do:

func remove(pathName, basePath string) {
    act := func(p pathwalker.PathInfo) error {
        if p.FileInfo.IsDir() {
            fmt.Printf("  Removing directory=%s\n", p.FullPath)
            return os.Remove(p.FullPath)
        }

        fmt.Printf("  Removing file=%s\n", p.FullPath)
        return os.Remove(p.FullPath)
    }

    pw := pathwalker.NewPathWalker(pathName, basePath)
    n, err := pw.Walk(act)
    fmt.Printf("Removed: %d/%d, err=%v\n", n, len(pw.VisitedPaths()), err)
}

If you just want to test whether a path is inside another path, you can do:

n, err := pathwalker.NewPathWalker(fileName, basePath).Walk(nil)
if n > 0 && err != nil {
    //is inside another path
}

Upvotes: 1

Related Questions