diff --git a/utils/fs.go b/utils/fs.go index 46f67b3..89ee87d 100644 --- a/utils/fs.go +++ b/utils/fs.go @@ -6,6 +6,8 @@ import ( "runtime" ) +const isWindows = runtime.GOOS == "windows" + // EnsureDirectory ensures that the given directoy exists and that is has the given permissions set. // If path is a file, it is deleted and a directory created. // If a directory is created, also all missing directories up to the required one are created with the given permissions. @@ -16,7 +18,7 @@ func EnsureDirectory(path string, perm os.FileMode) error { // file exists if f.IsDir() { // directory exists, check permissions - if runtime.GOOS == "windows" { + if isWindows { // TODO: set correct permission on windows // acl.Chmod(path, perm) } else if f.Mode().Perm() != perm { diff --git a/utils/structure.go b/utils/structure.go new file mode 100644 index 0000000..5a8b331 --- /dev/null +++ b/utils/structure.go @@ -0,0 +1,139 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "strings" + "sync" +) + +// DirStructure represents a directory structure with permissions that should be enforced. +type DirStructure struct { + sync.Mutex + + Path string + Dir string + Perm os.FileMode + Parent *DirStructure + Children map[string]*DirStructure +} + +// NewDirStructure returns a new DirStructure. +func NewDirStructure(path string, perm os.FileMode) *DirStructure { + return &DirStructure{ + Path: path, + Perm: perm, + Children: make(map[string]*DirStructure), + } +} + +// ChildDir adds a new child DirStructure and returns it. Should the child already exist, the existing child is returned and the permissions are updated. +func (ds *DirStructure) ChildDir(dirName string, perm os.FileMode) (child *DirStructure) { + ds.Lock() + defer ds.Unlock() + + // if exists, update + child, ok := ds.Children[dirName] + if ok { + child.Perm = perm + return child + } + + // create new + new := &DirStructure{ + Path: filepath.Join(ds.Path, dirName), + Dir: dirName, + Perm: perm, + Parent: ds, + Children: make(map[string]*DirStructure), + } + ds.Children[dirName] = new + return new +} + +// Ensure ensures that the specified directory structure (from the first parent on) exists. +func (ds *DirStructure) Ensure() error { + return ds.EnsureAbsPath(ds.Path) +} + +// EnsureRelPath ensures that the specified directory structure (from the first parent on) and the given relative path (to the DirStructure) exists. +func (ds *DirStructure) EnsureRelPath(dirPath string) error { + return ds.EnsureAbsPath(filepath.Join(ds.Path, dirPath)) +} + +// EnsureRelDir ensures that the specified directory structure (from the first parent on) and the given relative path (to the DirStructure) exists. +func (ds *DirStructure) EnsureRelDir(dirNames ...string) error { + return ds.EnsureAbsPath(filepath.Join(append([]string{ds.Path}, dirNames...)...)) +} + +// EnsureAbsPath ensures that the specified directory structure (from the first parent on) and the given absolute path exists. +// If the given path is outside the DirStructure, an error will be returned. +func (ds *DirStructure) EnsureAbsPath(dirPath string) error { + // always start at the top + if ds.Parent != nil { + return ds.Parent.EnsureAbsPath(dirPath) + } + + // check if root + if dirPath == ds.Path { + return ds.ensure(nil) + } + + // check scope + slashedPath := ds.Path + // add slash to end + if !strings.HasSuffix(slashedPath, string(filepath.Separator)) { + slashedPath += string(filepath.Separator) + } + // check if given path is in scope + if !strings.HasPrefix(dirPath, slashedPath) { + return fmt.Errorf(`path "%s" is outside of DirStructure scope`, dirPath) + } + + // get relative path + relPath, err := filepath.Rel(ds.Path, dirPath) + if err != nil { + return fmt.Errorf("failed to get relative path: %s", err) + } + + // split to path elements + pathDirs := strings.Split(filepath.ToSlash(relPath), "/") + + // start checking + return ds.ensure(pathDirs) +} + +func (ds *DirStructure) ensure(pathDirs []string) error { + ds.Lock() + defer ds.Unlock() + + // check current dir + err := EnsureDirectory(ds.Path, ds.Perm) + if err != nil { + return err + } + + if len(pathDirs) == 0 { + // we reached the end! + return nil + } + + child, ok := ds.Children[pathDirs[0]] + if !ok { + // we have reached the end of the defined dir structure + // ensure all remaining dirs + dirPath := ds.Path + for _, dir := range pathDirs { + dirPath = filepath.Join(dirPath, dir) + err := EnsureDirectory(dirPath, ds.Perm) + if err != nil { + return err + } + } + return nil + } + + // we got a child, continue + return child.ensure(pathDirs[1:]) +} diff --git a/utils/structure_test.go b/utils/structure_test.go new file mode 100644 index 0000000..d0c3e5a --- /dev/null +++ b/utils/structure_test.go @@ -0,0 +1,72 @@ +// +build !windows + +package utils + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" +) + +func ExampleDirStructure() { + // output: + // / [755] + // /repo [777] + // /repo/b [755] + // /repo/b/c [750] + // /repo/b/d [755] + // /repo/b/d/e [755] + // /repo/b/d/f [755] + // /secret [700] + + basePath, err := ioutil.TempDir("", "") + if err != nil { + fmt.Println(err) + return + } + + ds := NewDirStructure(basePath, 0755) + secret := ds.ChildDir("secret", 0700) + repo := ds.ChildDir("repo", 0777) + _ = repo.ChildDir("a", 0700) + b := repo.ChildDir("b", 0755) + c := b.ChildDir("c", 0750) + + err = ds.Ensure() + if err != nil { + fmt.Println(err) + } + + err = c.Ensure() + if err != nil { + fmt.Println(err) + } + + err = secret.Ensure() + if err != nil { + fmt.Println(err) + } + + err = b.EnsureRelDir("d", "e") + if err != nil { + fmt.Println(err) + } + + err = b.EnsureRelPath("d/f") + if err != nil { + fmt.Println(err) + } + + filepath.Walk(basePath, func(path string, info os.FileInfo, err error) error { + if err == nil { + dir := strings.TrimPrefix(path, basePath) + if dir == "" { + dir = "/" + } + fmt.Printf("%s [%o]\n", dir, info.Mode().Perm()) + } + return nil + }) +}