# fs | |||||
File system toolkit for Golang | |||||
FS | |||||
=== | |||||
[![Build Status](https://travis-ci.org/goanywhere/fs.svg?branch=master)](https://travis-ci.org/goanywhere/fs) [![GoDoc](https://godoc.org/github.com/goanywhere/fs?status.svg)](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) | |||||
} | |||||
} | |||||
} | |||||
} |