| # fs | |||||
| File system toolkit for Golang | |||||
| FS | |||||
| === | |||||
| [](https://travis-ci.org/goanywhere/fs) [](http://godoc.org/github.com/goanywhere/fs) | |||||
| FS is a file system toolkit for the [Go](http://golang.org) programming language. | |||||
| ## Getting Started | |||||
| Install the package, along with executable binary helper (**go 1.4** and greater is required): | |||||
| ```shell | |||||
| $ go get -v github.com/goanywhere/fs | |||||
| ``` |
| package fs | |||||
| import ( | |||||
| "bytes" | |||||
| "fmt" | |||||
| "io" | |||||
| "os" | |||||
| "os/exec" | |||||
| "path" | |||||
| "path/filepath" | |||||
| "regexp" | |||||
| "runtime" | |||||
| "strings" | |||||
| ) | |||||
| // Getcd returns absolute path of the caller. | |||||
| func Getcd(skip int) string { | |||||
| if dir := Geted(); strings.HasPrefix(dir, os.TempDir()) { | |||||
| pc, _, _, _ := runtime.Caller(skip + 1) | |||||
| function := runtime.FuncForPC(pc) | |||||
| filename, _ := function.FileLine(0) | |||||
| return path.Dir(filename) | |||||
| } else { | |||||
| return dir | |||||
| } | |||||
| } | |||||
| // Geted returns an absolute path to the executable. | |||||
| func Geted() string { | |||||
| if dir, err := filepath.Abs(filepath.Dir(os.Args[0])); err == nil { | |||||
| return dir | |||||
| } else { | |||||
| panic("Failed to retrieve executable directory") | |||||
| } | |||||
| } | |||||
| // Getwd returns a absolute path of the current directory. | |||||
| func Getwd() string { | |||||
| if cwd, err := os.Getwd(); err == nil { | |||||
| cwd, _ = filepath.Abs(cwd) | |||||
| return cwd | |||||
| } else { | |||||
| panic("Failed to retrieve current working directory") | |||||
| } | |||||
| } | |||||
| // Abs finds the absolute path for the given path. | |||||
| // Supported Formats: | |||||
| // * empty path => current working directory. | |||||
| // * '.', '..' & '~' | |||||
| // *NOTE* Abs does NOT check the existence of the path. | |||||
| func Abs(path string) string { | |||||
| var abs string | |||||
| cwd, _ := os.Getwd() | |||||
| if path == "" || path == "." { | |||||
| abs = cwd | |||||
| } else if path == ".." { | |||||
| abs = filepath.Join(cwd, path) | |||||
| } else if strings.HasPrefix(path, "~/") { | |||||
| abs = filepath.Join(UserDir(), path[2:]) | |||||
| } else if strings.HasPrefix(path, "./") { | |||||
| abs = filepath.Join(cwd, path[2:]) | |||||
| } else if strings.HasPrefix(path, "../") { | |||||
| abs = filepath.Join(cwd, "..", path[2:]) | |||||
| } else { | |||||
| return path | |||||
| } | |||||
| return abs | |||||
| } | |||||
| // Copy recursively copies files/(sub)directoires into the given path. | |||||
| // *NOTE* It uses platform's native copy commands (windows: copy, *nix: rsync). | |||||
| func Copy(src, dst string) (err error) { | |||||
| var cmd *exec.Cmd | |||||
| src, dst = Abs(src), Abs(dst) | |||||
| // Determine the command we need to use. | |||||
| if runtime.GOOS == "windows" { | |||||
| // *NOTE* Not sure this will work correctly, we don't have Windows to test. | |||||
| if IsFile(src) { | |||||
| cmd = exec.Command("copy", src, dst) | |||||
| } else { | |||||
| cmd = exec.Command("xcopy", src, dst, "/S /E") | |||||
| } | |||||
| } else { | |||||
| cmd = exec.Command("rsync", "-a", src, dst) | |||||
| } | |||||
| if stdout, err := cmd.StdoutPipe(); err == nil { | |||||
| if stderr, err := cmd.StderrPipe(); err == nil { | |||||
| // Start capturing the stdout/err. | |||||
| err = cmd.Start() | |||||
| io.Copy(os.Stdout, stdout) | |||||
| buffer := new(bytes.Buffer) | |||||
| buffer.ReadFrom(stderr) | |||||
| cmd.Wait() | |||||
| if cmd.ProcessState.String() != "exit status 0" { | |||||
| err = fmt.Errorf("\t%s\n", buffer.String()) | |||||
| } | |||||
| } | |||||
| } | |||||
| return | |||||
| } | |||||
| // Exists check if the given path exists. | |||||
| func Exists(path string) bool { | |||||
| if _, err := os.Stat(path); err != nil { | |||||
| if os.IsNotExist(err) { | |||||
| return false | |||||
| } | |||||
| } | |||||
| return true | |||||
| } | |||||
| // Find matches files with regular expression pattern under the given root. | |||||
| func Find(root string, pattern *regexp.Regexp) (paths []string) { | |||||
| if Exists(root) { | |||||
| filepath.Walk(root, func(path string, info os.FileInfo, e error) error { | |||||
| if pattern.MatchString(path) { | |||||
| paths = append(paths, info.Name()) | |||||
| } | |||||
| return e | |||||
| }) | |||||
| } | |||||
| return | |||||
| } | |||||
| // Grep searches text files via regular expression under the given path, | |||||
| // paths of the files contain matched line(s) will be returned. | |||||
| func Grep(root string, pattern *regexp.Regexp) (paths []string) { | |||||
| panic(fmt.Errorf("Not Implemented")) | |||||
| } | |||||
| // Glob recursively finds the names of all files matching pattern under the given path. | |||||
| func Glob(path string, pattern string) (matches []string, err error) { | |||||
| err = filepath.Walk(path, func(path string, info os.FileInfo, e error) error { | |||||
| if e == nil { | |||||
| if info.IsDir() { | |||||
| if filenames, e := filepath.Glob(filepath.Join(path, pattern)); e == nil { | |||||
| matches = append(matches, filenames...) | |||||
| } | |||||
| } | |||||
| } | |||||
| return e | |||||
| }) | |||||
| return | |||||
| } | |||||
| // IsDir checks if the given path is a directory. | |||||
| func IsDir(path string) bool { | |||||
| src, err := os.Stat(path) | |||||
| if os.IsNotExist(err) { | |||||
| return false | |||||
| } | |||||
| return src.IsDir() | |||||
| } | |||||
| // IsFile checks if the given path is a file. | |||||
| func IsFile(path string) bool { | |||||
| src, err := os.Stat(path) | |||||
| if os.IsNotExist(err) { | |||||
| return false | |||||
| } | |||||
| return !src.IsDir() | |||||
| } | |||||
| // UserDir finds base path of current system user. | |||||
| func UserDir() string { | |||||
| if runtime.GOOS == "windows" { | |||||
| home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH") | |||||
| if home == "" { | |||||
| home = os.Getenv("USERPROFILE") | |||||
| } | |||||
| return home | |||||
| } | |||||
| return os.Getenv("HOME") | |||||
| } |
| package fs | |||||
| import ( | |||||
| "bufio" | |||||
| "os" | |||||
| "os/exec" | |||||
| "testing" | |||||
| . "github.com/smartystreets/goconvey/convey" | |||||
| ) | |||||
| func setup(handler func(f string)) { | |||||
| filename := "/tmp/tmpfile" | |||||
| if file, err := os.Create(filename); err == nil { | |||||
| defer file.Close() | |||||
| defer os.Remove(filename) | |||||
| buffer := bufio.NewWriter(file) | |||||
| buffer.WriteString("I'm just a temp. file") | |||||
| buffer.Flush() | |||||
| handler(filename) | |||||
| } | |||||
| } | |||||
| func TestAbs(t *testing.T) { | |||||
| Convey("Absolute path check", t, func() { | |||||
| So(Abs("/tmp"), ShouldEqual, "/tmp") | |||||
| }) | |||||
| } | |||||
| func TestCopy(t *testing.T) { | |||||
| Convey("Copy files/directories recursively", t, func() { | |||||
| filename := "GoAnywhereFake" | |||||
| exec.Command("touch", Abs("~/"+filename)).Run() | |||||
| defer os.Remove("/tmp/" + filename) | |||||
| err := Copy("~/"+filename, "/tmp") | |||||
| So(Exists("/tmp/"+filename), ShouldBeTrue) | |||||
| So(err, ShouldBeNil) | |||||
| exec.Command("mkdir", Abs("~/GoAnywhere")).Run() | |||||
| exec.Command("touch", Abs("~/GoAnywhere/Fake")).Run() | |||||
| defer os.RemoveAll("~/GoAnywhere") | |||||
| err = Copy("~/GoAnywhere", "/tmp") | |||||
| So(Exists("/tmp/GoAnywhere"), ShouldBeTrue) | |||||
| So(err, ShouldBeNil) | |||||
| }) | |||||
| } | |||||
| func TestExists(t *testing.T) { | |||||
| Convey("Checks if the given path exists", t, func() { | |||||
| exists := Exists("/tmp") | |||||
| So(exists, ShouldBeTrue) | |||||
| exists = Exists("/NotExists") | |||||
| So(exists, ShouldBeFalse) | |||||
| }) | |||||
| } | |||||
| func TestIsDir(t *testing.T) { | |||||
| setup(func(filename string) { | |||||
| flag := IsDir(filename) | |||||
| Convey("Checks if the given path is a directory", t, func() { | |||||
| So(flag, ShouldBeFalse) | |||||
| }) | |||||
| }) | |||||
| flag := IsDir("/tmp") | |||||
| Convey("Checks if the given path is a directory", t, func() { | |||||
| So(flag, ShouldBeTrue) | |||||
| }) | |||||
| } | |||||
| func TestIsFile(t *testing.T) { | |||||
| setup(func(filename string) { | |||||
| flag := IsFile(filename) | |||||
| Convey("Checks if the given path is a file", t, func() { | |||||
| So(flag, ShouldBeTrue) | |||||
| }) | |||||
| }) | |||||
| flag := IsFile("/tmp") | |||||
| Convey("Checks if the given path is a file", t, func() { | |||||
| So(flag, ShouldBeFalse) | |||||
| }) | |||||
| } |
| package fs | |||||
| import ( | |||||
| "os" | |||||
| "path/filepath" | |||||
| "regexp" | |||||
| "sync" | |||||
| "time" | |||||
| log "github.com/Sirupsen/logrus" | |||||
| "github.com/go-fsnotify/fsnotify" | |||||
| ) | |||||
| type watcher struct { | |||||
| dir string | |||||
| mutex sync.Mutex | |||||
| daemon *fsnotify.Watcher | |||||
| event chan *fsnotify.Event | |||||
| ignores []string | |||||
| watchList map[*regexp.Regexp]func(string) | |||||
| } | |||||
| func NewWatcher(dir string) *watcher { | |||||
| self := new(watcher) | |||||
| self.dir = dir | |||||
| self.event = make(chan *fsnotify.Event) | |||||
| self.ignores = []string{} | |||||
| self.watchList = make(map[*regexp.Regexp]func(string)) | |||||
| return self | |||||
| } | |||||
| // IsWrite checks if the triggered event is fsnotify.Write|fsnotify.Create. | |||||
| func (self *watcher) isWrite(event *fsnotify.Event) bool { | |||||
| // instead of MODIFY event, editors may only send CREATE. | |||||
| // so we need to capture write & create. | |||||
| if event.Op&fsnotify.Write == fsnotify.Write || | |||||
| event.Op&fsnotify.Create == fsnotify.Create { | |||||
| return true | |||||
| } | |||||
| return false | |||||
| } | |||||
| // IsRemove checks if the triggered event is fsnotify.Remove. | |||||
| func (self *watcher) isRemove(event *fsnotify.Event) bool { | |||||
| return event.Op&fsnotify.Remove == fsnotify.Remove | |||||
| } | |||||
| // Add appends regular expression based pattern processor into the watch list. | |||||
| func (self *watcher) Add(pattern *regexp.Regexp, process func(path string)) { | |||||
| self.mutex.Lock() | |||||
| defer self.mutex.Unlock() | |||||
| self.watchList[pattern] = process | |||||
| } | |||||
| func (self *watcher) watch() { | |||||
| self.daemon, _ = fsnotify.NewWatcher() | |||||
| if self.daemon != nil { | |||||
| self.daemon.Close() | |||||
| } | |||||
| // ensure we have a new daemon watcher eachtime we start watching. | |||||
| self.daemon, _ = fsnotify.NewWatcher() | |||||
| if err := self.daemon.Add(self.dir); err != nil { | |||||
| log.Fatalf("Failed to create fs watcher for <%s>: %v", self.dir, err) | |||||
| } | |||||
| // watch all folders under the root. | |||||
| filepath.Walk(self.dir, func(path string, info os.FileInfo, e error) error { | |||||
| if info.IsDir() { | |||||
| for _, ignore := range self.ignores { | |||||
| if info.Name() == ignore { | |||||
| return filepath.SkipDir | |||||
| } | |||||
| } | |||||
| if err := self.daemon.Add(path); err != nil { | |||||
| log.Fatalf("Failed create watch list for (%s): %v", info.Name(), err) | |||||
| } | |||||
| } | |||||
| return e | |||||
| }) | |||||
| } | |||||
| func (self *watcher) startWatching() { | |||||
| self.watch() | |||||
| var evt *fsnotify.Event | |||||
| // multiple events can be triggered on a successful write | |||||
| // (e.g. Create followed by multiple CHMOD), just postpone | |||||
| // a bit to let it calm before actual processing. | |||||
| var delay <-chan time.Time | |||||
| for { | |||||
| select { | |||||
| case event := <-self.daemon.Events: | |||||
| // We only need "Write" event (modify | create | remove) | |||||
| if self.isWrite(&event) || self.isRemove(&event) { | |||||
| evt = &event | |||||
| delay = time.After(500 * time.Millisecond) | |||||
| } | |||||
| case err := <-self.daemon.Errors: | |||||
| log.Fatalf("Failed to watch the path %v", err) | |||||
| case <-delay: | |||||
| self.event <- evt | |||||
| } | |||||
| } | |||||
| } | |||||
| // Start watches all file changes under the root path & dispatch | |||||
| // to corresonding handlers (added via Add function) | |||||
| func (self *watcher) Start() { | |||||
| go self.startWatching() | |||||
| // listens the catched event & start processing. | |||||
| for event := range self.event { | |||||
| if event == nil { | |||||
| continue | |||||
| } | |||||
| // start processing the event | |||||
| var filename = filepath.Base(event.Name) | |||||
| for pattern, process := range self.watchList { | |||||
| if pattern.MatchString(filename) { | |||||
| process(event.Name) | |||||
| } | |||||
| } | |||||
| } | |||||
| } |