// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "os" "path/filepath" ) // FileDelAll recursevely removes file `name` from the `root` // directory. It is useful to get truly empty directories after // cloning checked out almost empty directories containing // a stake file like `.gitkeep` func FileDelAll(root, name string) error { return filepath.Walk(root, func(p string, i os.FileInfo, err error) error { if err != nil { return (err) } if filepath.Base(p) == name { if os.Remove(p) != nil { return (err) } } return nil }) }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "os" ) // FileInfoPath is a wrapper of os.FileInfo with an additional // field to store the path to the file of interest type FileInfoPath struct { os.FileInfo path string } // NewFileInfoPath creates new FileInfoPath struct func NewFileInfoPath(path string) (*FileInfoPath, error) { fi, err := os.Stat(path) if err != nil { return &FileInfoPath{nil, path}, err } return &FileInfoPath{fi, path}, nil } // MakeFipSlice creates a slice of *FileInfoPaths based on // provided list of file names. It returns the first // encountered error. func MakeFipSlice(files ...string) ([]*FileInfoPath, error) { fips := make([]*FileInfoPath, 0) for _, name := range files { fip, err := NewFileInfoPath(name) if err != nil { return nil, err } fips = append(fips, fip) } return fips, nil } // Path returns the stored full path in the FileInfoPath struct func (fip *FileInfoPath) Path() string { return fip.path }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "bufio" "os" "testing" "time" ) // FileRank is the signature of functions which are // provided to TreeDiff to compare two *FileInfoPath structs // and related files. // // When comparing filesystem objects, the algorithm used in // the TreeDiff function expects the "less-than" logic from // comparator functions. It means, that a comparator is expected // to return "true" if and only if the "left" parameter is // strictly less than the "right" parameter according to the // comparator's criteria. // // FileRank does not provide an error handling interface. If // needed, it can be implemented via a closure. See the // ByContent comparator generator for an example of it. type FileRank func(left, right *FileInfoPath) bool // ByName is basic for comparing directories and should // be provided as a first comparator in most cases func ByName(left, right *FileInfoPath) bool { return left.Name() < right.Name() } // ByDir differentiates directorries vs. files and puts // directories earlier in a sort order func ByDir(left, right *FileInfoPath) bool { return left.IsDir() && !right.IsDir() } // BySize compares sizes of files, given that both of the // files are regular files as opposed to not directories, etc. func BySize(left, right *FileInfoPath) bool { return left.Mode().IsRegular() && right.Mode().IsRegular() && (left.Size() < right.Size()) } // ByTime compares files' last modification times with up to // 10µs precision to accommodate filesyustem quirks func ByTime(left, right *FileInfoPath) bool { return left.ModTime().Before(right.ModTime().Add(-10 * time.Microsecond)) } // ByPerm compares bits 0-8 of Unix-like file permissions func ByPerm(left, right *FileInfoPath) bool { return left.Mode().Perm() < right.Mode().Perm() } // ByContent returns a function which compares files' // content without first comparing sizes. For example, // file containing "aaa" will rank as lesser than the one // containing "ab" even though it is opposite to their sizes. // To consider sizes first, make sure to specify the BySize // comparator earlier in the chain. func ByContent(t *testing.T) FileRank { return func(left, right *FileInfoPath) bool { leftF, err := os.Open(left.Path()) if err != nil { t.Fatal(err) } defer leftF.Close() rightF, err := os.Open(right.Path()) if err != nil { t.Fatal(err) } defer rightF.Close() leftBuf := bufio.NewReader(leftF) rightBuf := bufio.NewReader(rightF) for { rByte, err := rightBuf.ReadByte() if err != nil { return false } lByte, err := leftBuf.ReadByte() if err != nil { return true } if lByte == rByte { continue } if lByte < rByte { return true } return false } } } // Less applies provided comparators to the pair of *FileInfoPath structs. func Less(left, right *FileInfoPath, cmps ...FileRank) bool { for _, less := range cmps { if less(left, right) { return true } } return false }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "io" "io/ioutil" "log" "os" "path/filepath" ) // TempInitDir function creates a directory for holding // temporary files according to platform preferences and // returns the directory name and a cleanup function. // // The returned values are: // // 1. a string containing the created temporary directory path // // 2. a cleanup function to change back to the old working // directory and to delete the temporary directory // // 3. an error // // If there was an error while creating the temporary // directory, then the returned directory name is empty, // cleanup funcion is nil, and the temp folder is // expected to be already removed. func TempInitDir() (string, func(), error) { root, err := ioutil.TempDir("", "") if err != nil { os.RemoveAll(root) return "", nil, err } return root, func() { dirs := make([]string, 0) err := filepath.Walk( root, func(fn string, fi os.FileInfo, er error) error { if fi.IsDir() { err = os.Chmod(fn, 0700) if err != nil { return err } dirs = append(dirs, fn) return nil } return os.Remove(fn) }) if err != nil { log.Fatalln(err) } for i := len(dirs) - 1; i >= 0; i-- { err = os.RemoveAll(dirs[i]) if err != nil { log.Fatalln(err) } } }, nil } // TempInitChdir creates a temporary directory in the same // fashion as TempInitDir. It also changes into the newly // created temporary directory and adds returning back // to the old working directory to the returned cleanup // function. The returned values are: // // 1. a string containing the previous working directory // // 2. a cleanup function to change back to the old working // directory and to delete the temporary directory // // 3. an error func TempInitChdir() (string, func(), error) { root, cleanup, err := TempInitDir() if err != nil { return "", nil, err } wd, err := os.Getwd() if err != nil { cleanup() return "", nil, err } err = os.Chdir(root) if err != nil { cleanup() return "", nil, err } return wd, func() { os.Chdir(wd) cleanup() }, nil } // TempCloneDir function creates a copy of an existing // directory with it's content - regular files only - for // holding temporary test files. // // The returned values are: // // 1. a string containing the created temporary directory path // // 2. a cleanup function to change back to the old working // directory and to delete the temporary directory // // 3. an error // // If there was an error while cloning the temporary // directory, then the returned directory name is empty, // cleanup funcion is nil, and the temp folder is // expected to be already removed. // // The clone attempts to maintain the basic original Unix // permissions (9-bit only, from the rxwrwxrwx set). // If, however, the user does not have read permission // for a file, or read+execute permission for a directory, // then the clone process will naturally fail. func TempCloneDir(src string) (string, func(), error) { root, cleanup, err := TempInitDir() if err != nil { return "", nil, err } err = TreeCopy(src, root) if err != nil { cleanup() return "", nil, err } return root, cleanup, nil } // TempCloneChdir clones a temporary directory in the same // fashion as TempCloneDir. It also changes into the newly // cloned temporary directory and adds returning back // to the old working directory to the returned cleanup // function. The returned values are: // // 1. a string containing the previous working directory // // 2. a cleanup function to change back to the old working // directory and to delete the temporary directory // // 3. an error func TempCloneChdir(src string) (string, func(), error) { root, cleanup, err := TempCloneDir(src) if err != nil { return "", nil, err } wd, err := os.Getwd() if err != nil { cleanup() return "", nil, err } err = os.Chdir(root) if err != nil { cleanup() return "", nil, err } return wd, func() { os.Chdir(wd) cleanup() }, nil } // TempCreateChdir is a combination of `TempInitChdir` and // `TreeCreate` functions. It creates a termporary directory, // changes into it, populates it fron the provided `config` // as `TreeCreate` would, and returns the old directory name // and the cleanup function. func TempCreateChdir(config io.Reader) (string, func(), error) { old, cleanup, err := TempInitChdir() if err != nil { return "", nil, err } err = TreeCreate(config) if err != nil { cleanup() return "", nil, err } return old, cleanup, nil }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "io" "os" "path/filepath" "time" ) type dirEntry struct { name string perm os.FileMode time time.Time } // TreeCopy duplicates redular files and directories from // inside the source directory into an existing destination // directory. func TreeCopy(src, dst string) error { srcClean := filepath.Clean(src) srcLen := len(srcClean) dirs := make([]*dirEntry, 0) err := filepath.Walk( srcClean, func(fn string, fi os.FileInfo, er error) error { if er != nil || len(fn) <= srcLen { return er } dest := filepath.Join(dst, fn[srcLen:]) if fi.Mode().IsRegular() { srcf, err := os.Open(fn) if err != nil { return err } dstf, err := os.OpenFile(dest, os.O_CREATE|os.O_WRONLY, 0600) if err != nil { return err } _, err = io.Copy(dstf, srcf) if err != nil { return err } err = srcf.Close() if err != nil { return err } err = dstf.Close() if err != nil { return err } err = os.Chmod(dest, fi.Mode()) if err != nil { return err } destMT := fi.ModTime() return os.Chtimes(dest, destMT, destMT) } if fi.Mode().IsDir() { dirs = append(dirs, &dirEntry{dest, fi.Mode().Perm(), fi.ModTime()}) return os.Mkdir(dest, 0700) } return nil }) if err != nil { return err } for i := len(dirs) - 1; i >= 0; i-- { err := os.Chmod(dirs[i].name, dirs[i].perm) if err != nil { return err } err = os.Chtimes(dirs[i].name, dirs[i].time, dirs[i].time) if err != nil { return err } } return nil }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "bufio" "io" "os" "regexp" "strconv" "time" ) type emptyErr struct { error } // TreeCreate parses a suplied Reader for the tree information // and follows the instructions to create files and directories. // // The input has line records with three or four fields // separated by one or more tabs. White space is trimmed on // both ends of lines. Empty lines are skipped. The general // line format is: // // <1. time> <2. permissions> <3. name> <4. optional content> // // Field 1: Time in RFC3339 format, as shown at // https://golang.org/pkg/time/#RFC3339 // // Field 2: Octal (required) representation of FileMode, as at // https://golang.org/pkg/os/#FileMode // // Field 3: is the file or directory path to be created. If the // first character of the path is a double-quote or a back-tick, // then the path wil be passed through strconv.Unquote() function. // It allows for using tab-containing or otherwise weird names. // The quote or back-tick should be balanced at the end of // the field. // // If the path in Field 3 ends with a forward slash, then it is // treated as a directory, otherwise - as a regular file. // // Field 4: is optional content to be written into the file. It // follows the same quotation rules as paths in Field 3. // Directory entries ignore Field 4 if present. // // It is up to the caller to deal with conflicting file and // directory names in the input. TreeCreate processes the // input line-by-line and will return with error at a first // problem it runs into. func TreeCreate(config io.Reader) error { dirs := make([]*dirEntry, 0) scanner := bufio.NewScanner(config) for scanner.Scan() { mt, perm, name, content, err := parse(scanner.Text()) if err != nil { if _, ok := err.(*emptyErr); ok { continue } return err } if name[len(name)-1] == '/' { name = name[:len(name)-1] if err = os.Mkdir(name, 0700); err != nil { return err } dirs = append(dirs, &dirEntry{name, perm, mt}) continue } f, err := os.Create(name) if err != nil { return err } if len(content) > 0 { _, err = f.WriteString(content) if err != nil { return err } } err = f.Close() if err != nil { return err } err = os.Chmod(name, perm) if err != nil { return err } err = os.Chtimes(name, mt, mt) if err != nil { return err } } err := scanner.Err() if err != nil { return err } for i := len(dirs) - 1; i >= 0; i-- { err := os.Chmod(dirs[i].name, dirs[i].perm) if err != nil { return err } err = os.Chtimes(dirs[i].name, dirs[i].time, dirs[i].time) if err != nil { return err } } return nil } var ( re = regexp.MustCompile(`^\s*([-0-9T:Z]+)\t+(0[0-7]{0,4})\t+([^\t]+)(\t+([^\t]+))?\s*$`) empty = regexp.MustCompile(`^\s*$`) ) func parse(line string) (time.Time, os.FileMode, string, string, error) { if empty.MatchString(line) { return time.Time{}, 0, "", "", &emptyErr{} } parts := re.FindStringSubmatch(line) mt, err := time.Parse(time.RFC3339, parts[1]) if err != nil { return time.Time{}, 0, "", "", err } mt = mt.Round(0) perm64, err := strconv.ParseUint(parts[2], 8, 32) if err != nil { return time.Time{}, 0, "", "", err } perm := os.FileMode(perm64) var path string if parts[3][0] == '`' || parts[3][0] == '"' { path, err = strconv.Unquote(parts[3]) if err != nil { return time.Time{}, 0, "", "", err } } else { path = parts[3] } var content string if len(parts[5]) > 0 { if parts[5][0] == '`' || parts[5][0] == '"' { content, err = strconv.Unquote(parts[5]) if err != nil { return time.Time{}, 0, "", "", err } } else { content = parts[5] } } return mt, perm, path, content, nil }
// Copyright 2017 Vlad Didenko. All rights reserved. // See the included LICENSE.md file for licensing information package fst // import "go.didenko.com/fst" import ( "fmt" "os" "path/filepath" ) // TreeDiff produces a slice of human-readable notes about // recursive differences between two directory trees on a // filesystem. Only plan directories and plain files are // compared in the tree. Specific comparisons are determined // By the variadic slice of FileRank functions, like the // ones in this package. A commonly used set of comparators // is ByName, ByDir, BySize, and ByContent func TreeDiff(a string, b string, comps ...FileRank) ([]string, error) { listA, err := collectFileInfo(a) if err != nil { return nil, err } listB, err := collectFileInfo(b) if err != nil { return nil, err } onlyA, onlyB := collectDifferent(listA, listB, comps...) var diags []string if len(onlyA) > 0 { diagA := fmt.Sprintf("Unique items from \"%s\": \n", a) for _, fi := range onlyA { diagA = diagA + fmt.Sprintf("dir:%v, sz:%v, mode:%v, time:%v, name: %v\n", fi.IsDir(), fi.Size(), fi.Mode(), fi.ModTime(), fi.Name()) } diags = append(diags, diagA) } if len(onlyB) > 0 { diagB := fmt.Sprintf("Unique items from \"%s\": \n", b) for _, fi := range onlyB { diagB = diagB + fmt.Sprintf("dir:%v, sz:%v, mode:%v, time:%v, name: %v\n", fi.IsDir(), fi.Size(), fi.Mode(), fi.ModTime(), fi.Name()) } diags = append(diags, diagB) } return diags, nil } // collectDifferent forms file infomation slices for files // unique to either left or right collections. It is based // on a modified algorithm from the go.didenko.com/slops package func collectDifferent(left, right []*FileInfoPath, comps ...FileRank) (onlyLeft, onlyRight []*FileInfoPath) { onlyLeft = make([]*FileInfoPath, 0) onlyRight = make([]*FileInfoPath, 0) for l, r := 0, 0; l < len(left) || r < len(right); { if r < len(right) && (l == len(left) || Less(right[r], left[l], comps...)) { onlyRight = append(onlyRight, right[r]) r++ continue } if l < len(left) && (r == len(right) || Less(left[l], right[r], comps...)) { onlyLeft = append(onlyLeft, left[l]) l++ continue } l++ r++ } return onlyLeft, onlyRight } func collectFileInfo(dir string) ([]*FileInfoPath, error) { list := make([]*FileInfoPath, 0) err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error { if err == nil && path != dir { list = append(list, &FileInfoPath{f, path}) } return err }) return list, err }