@@ -1,2 +1,13 @@ | |||
# 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 | |||
``` |
@@ -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) | |||
} | |||
} | |||
} | |||
} |