| @@ -1,2 +1,13 @@ | |||
| # 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 | |||
| ``` | |||
| @@ -0,0 +1,183 @@ | |||
| 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") | |||
| } | |||
| @@ -0,0 +1,85 @@ | |||
| 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) | |||
| }) | |||
| } | |||
| @@ -0,0 +1,126 @@ | |||
| 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) | |||
| } | |||
| } | |||
| } | |||
| } | |||