diff --git a/Makefile b/Makefile index ae2f5d1..ce416b2 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ TAGS ?= "" GO_BIN ?= "go" -install: - $(GO_BIN) install -tags ${TAGS} -v ./pkger/cmd/pkger +install: + cd ./cmd/pkger && $(GO_BIN) install -tags ${TAGS} -v . make tidy tidy: @@ -16,18 +16,18 @@ deps: $(GO_BIN) get -tags ${TAGS} -t ./... make tidy -build: +build: $(GO_BIN) build -v . make tidy -test: +test: $(GO_BIN) test -cover -tags ${TAGS} ./... make tidy -ci-deps: +ci-deps: $(GO_BIN) get -tags ${TAGS} -t ./... -ci-test: +ci-test: $(GO_BIN) test -tags ${TAGS} -race ./... lint: @@ -47,7 +47,7 @@ endif make install make tidy -release-test: +release-test: $(GO_BIN) test -tags ${TAGS} -race ./... make tidy diff --git a/cmd/pkger/go.mod b/cmd/pkger/go.mod new file mode 100644 index 0000000..163a559 --- /dev/null +++ b/cmd/pkger/go.mod @@ -0,0 +1,7 @@ +module github.com/markbates/pkger/cmd/pkger + +go 1.12 + +require github.com/markbates/pkger v0.0.0 + +replace github.com/markbates/pkger => ../../ diff --git a/cmd/pkger/go.sum b/cmd/pkger/go.sum new file mode 100644 index 0000000..9f71dc6 --- /dev/null +++ b/cmd/pkger/go.sum @@ -0,0 +1,2 @@ +github.com/gobuffalo/here v0.2.0 h1:tbOsO8QVUL5MT4swc0JnqZ7IlUm09e6vXYxNSMhOYMw= +github.com/gobuffalo/here v0.2.0/go.mod h1:2a6G14FaAKOGJMK/5UNa4Og/+iyFS5cq3MnlvFR7YDk= diff --git a/cmd/pkger/info.go b/cmd/pkger/info.go new file mode 100644 index 0000000..c935147 --- /dev/null +++ b/cmd/pkger/info.go @@ -0,0 +1,40 @@ +package main + +import ( + "fmt" + + "github.com/markbates/pkger" +) + +func info(args []string) error { + if len(args) == 0 { + args = []string{"."} + } + for _, a := range args { + f, err := pkger.Open(a) + if err != nil { + return err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return err + } + + if fi.IsDir() { + files, err := f.Readdir(-1) + if err != nil { + return err + } + for _, ff := range files { + fmt.Println(pkger.NewFileInfo(ff)) + } + continue + } + + fmt.Println(pkger.NewFileInfo(fi)) + } + + return nil +} diff --git a/cmd/pkger/main.go b/cmd/pkger/main.go new file mode 100644 index 0000000..0238cf4 --- /dev/null +++ b/cmd/pkger/main.go @@ -0,0 +1,34 @@ +package main + +import ( + "log" + "os" +) + +func main() { + type ex func([]string) error + + args := os.Args[1:] + + if len(args) == 0 { + log.Fatal("does not compute") + } + + var fn ex + switch args[0] { + case "walk": + fn = walk + case "read": + fn = read + case "info": + fn = info + case "serve": + fn = serve + } + if fn == nil { + log.Fatal(args) + } + if err := fn(args[1:]); err != nil { + log.Fatal(err) + } +} diff --git a/cmd/pkger/read.go b/cmd/pkger/read.go new file mode 100644 index 0000000..ac1918d --- /dev/null +++ b/cmd/pkger/read.go @@ -0,0 +1,41 @@ +package main + +import ( + "fmt" + "io" + "os" + + "github.com/markbates/pkger" +) + +func read(args []string) error { + if len(args) == 0 { + args = []string{"."} + } + for _, a := range args { + fmt.Printf("### cmd/pkger/read.go:16 a (%T) -> %q %+v\n", a, a, a) + f, err := pkger.Open(a) + if err != nil { + return err + } + defer f.Close() + + fmt.Println(f.Path()) + + fi, err := f.Stat() + if err != nil { + return err + } + + if fi.IsDir() { + return fmt.Errorf("can not read a dir %s", a) + } + + _, err = io.Copy(os.Stdout, f) + if err != nil { + return err + } + } + + return nil +} diff --git a/cmd/pkger/serve.go b/cmd/pkger/serve.go new file mode 100644 index 0000000..dd69d38 --- /dev/null +++ b/cmd/pkger/serve.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + "log" + "net/http" + + "github.com/markbates/pkger" +) + +func serve(args []string) error { + if len(args) == 0 { + args = []string{"."} + } + f, err := pkger.Open(args[0]) + if err != nil { + log.Fatal("1", err) + } + defer f.Close() + + fmt.Println(f.Path()) + + return http.ListenAndServe(":3000", http.FileServer(f)) +} diff --git a/cmd/pkger/walk.go b/cmd/pkger/walk.go new file mode 100644 index 0000000..03e8e82 --- /dev/null +++ b/cmd/pkger/walk.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + + "github.com/markbates/pkger" +) + +func walk(args []string) error { + err := pkger.Walk(".", func(path pkger.Path, info os.FileInfo, err error) error { + if err != nil { + return err + } + fmt.Println(path) + return nil + }) + return err +} diff --git a/file.go b/file.go new file mode 100644 index 0000000..2925c7b --- /dev/null +++ b/file.go @@ -0,0 +1,142 @@ +package pkger + +import ( + "bytes" + "encoding/json" + "io" + "net/http" + "os" + "path/filepath" + "time" + + "github.com/gobuffalo/here" +) + +const timeFmt = time.RFC3339Nano + +type File struct { + info *FileInfo + her here.Info + path Path + data []byte + index *index + Source io.ReadCloser +} + +func (f *File) Open(name string) (http.File, error) { + if f.index == nil { + f.index = &index{ + Files: map[Path]*File{}, + } + } + pt, err := Parse(name) + if err != nil { + return nil, err + } + + if len(pt.Pkg) == 0 { + pt.Pkg = f.path.Pkg + } + + h := httpFile{} + + if pt == f.path { + h.File = f + } else { + of, err := f.index.Open(pt) + if err != nil { + return nil, err + } + defer of.Close() + h.File = of + } + + if len(f.data) > 0 { + h.crs = &byteCRS{bytes.NewReader(f.data)} + return h, nil + } + + bf, err := os.Open(h.File.Path()) + if err != nil { + return h, err + } + fi, err := bf.Stat() + if err != nil { + return h, err + } + if fi.IsDir() { + return h, nil + } + + if err != nil { + return nil, err + } + + h.crs = bf + return h, nil +} + +func (f File) Stat() (os.FileInfo, error) { + if f.info == nil { + return nil, os.ErrNotExist + } + return f.info, nil +} + +func (f *File) Close() error { + if f.Source == nil { + return nil + } + if c, ok := f.Source.(io.Closer); ok { + return c.Close() + } + return nil +} + +func (f File) Name() string { + return f.info.Name() +} + +func (f File) Path() string { + dir := f.her.Dir + if filepath.Base(dir) == f.Name() { + return dir + } + fp := filepath.Join(dir, f.Name()) + return fp +} + +func (f File) String() string { + if f.info == nil { + return "" + } + b, _ := json.MarshalIndent(f.info, "", " ") + return string(b) +} + +func (f *File) Read(p []byte) (int, error) { + if f.Source != nil { + return f.Source.Read(p) + } + + of, err := os.Open(f.Path()) + if err != nil { + return 0, err + } + f.Source = of + return f.Source.Read(p) +} + +// Readdir reads the contents of the directory associated with file and returns a slice of up to n FileInfo values, as would be returned by Lstat, in directory order. Subsequent calls on the same file will yield further FileInfos. +// +// If n > 0, Readdir returns at most n FileInfo structures. In this case, if Readdir returns an empty slice, it will return a non-nil error explaining why. At the end of a directory, the error is io.EOF. +// +// If n <= 0, Readdir returns all the FileInfo from the directory in a single slice. In this case, if Readdir succeeds (reads all the way to the end of the directory), it returns the slice and a nil error. If it encounters an error before the end of the directory, Readdir returns the FileInfo read until that point and a non-nil error. +func (f *File) Readdir(count int) ([]os.FileInfo, error) { + of, err := os.Open(f.Path()) + if err != nil { + return nil, err + } + defer of.Close() + return of.Readdir(count) +} diff --git a/file_info.go b/file_info.go new file mode 100644 index 0000000..1c7752a --- /dev/null +++ b/file_info.go @@ -0,0 +1,124 @@ +package pkger + +import ( + "encoding/json" + "os" + "time" +) + +type FileInfo struct { + name string + size int64 + mode os.FileMode + modTime time.Time + isDir bool + sys interface{} +} + +func (f *FileInfo) String() string { + b, _ := json.MarshalIndent(f, "", " ") + return string(b) +} + +func (f *FileInfo) MarshalJSON() ([]byte, error) { + return json.Marshal(map[string]interface{}{ + "name": f.name, + "size": f.size, + "mode": f.mode, + "modTime": f.modTime.Format(timeFmt), + "isDir": f.isDir, + "sys": f.sys, + }) +} + +// func (f *FileInfo) UnmarshalJSON(b []byte) error { +// m := map[string]interface{}{} +// if err := json.Unmarshal(b, &m); err != nil { +// return err +// } +// +// var ok bool +// +// f.name, ok = m["name"].(string) +// if !ok { +// return fmt.Errorf("could not determine name %q", m["name"]) +// } +// +// size, ok := m["size"].(float64) +// if !ok { +// return fmt.Errorf("could not determine size %q", m["size"]) +// } +// f.size = int64(size) +// +// mode, ok := m["mode"].(float64) +// if !ok { +// return fmt.Errorf("could not determine mode %q", m["mode"]) +// } +// f.mode = os.FileMode(mode) +// +// modTime, ok := m["modTime"].(string) +// if !ok { +// return fmt.Errorf("could not determine modTime %q", m["modTime"]) +// } +// t, err := time.Parse(timeFmt, modTime) +// if err != nil { +// return err +// } +// f.modTime = t +// +// f.isDir, ok = m["isDir"].(bool) +// if !ok { +// return fmt.Errorf("could not determine isDir %q", m["isDir"]) +// } +// f.sys = m["sys"] +// return nil +// } + +func (f *FileInfo) Name() string { + return f.name +} + +func (f *FileInfo) Size() int64 { + return f.size +} + +func (f *FileInfo) Mode() os.FileMode { + return f.mode +} + +func (f *FileInfo) ModTime() time.Time { + return f.modTime +} + +func (f *FileInfo) IsDir() bool { + return f.isDir +} + +func (f *FileInfo) Sys() interface{} { + return f.sys +} + +var _ os.FileInfo = &FileInfo{} + +func NewFileInfo(info os.FileInfo) *FileInfo { + fi := &FileInfo{ + name: info.Name(), + size: info.Size(), + mode: info.Mode(), + modTime: info.ModTime(), + isDir: info.IsDir(), + sys: info.Sys(), + } + return fi +} + +func WithName(name string, info os.FileInfo) *FileInfo { + if ft, ok := info.(*FileInfo); ok { + ft.name = name + return ft + } + + fo := NewFileInfo(info) + fo.name = name + return fo +} diff --git a/go.mod b/go.mod index 2b5ac77..9ce7a9d 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/markbates/pkger go 1.12 + +require github.com/gobuffalo/here v0.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..9f71dc6 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gobuffalo/here v0.2.0 h1:tbOsO8QVUL5MT4swc0JnqZ7IlUm09e6vXYxNSMhOYMw= +github.com/gobuffalo/here v0.2.0/go.mod h1:2a6G14FaAKOGJMK/5UNa4Og/+iyFS5cq3MnlvFR7YDk= diff --git a/http.go b/http.go new file mode 100644 index 0000000..446c5d6 --- /dev/null +++ b/http.go @@ -0,0 +1,45 @@ +package pkger + +import ( + "bytes" + "io" + "net/http" + "os" +) + +type crs interface { + io.Closer + io.Reader + io.Seeker +} + +type byteCRS struct { + *bytes.Reader +} + +func (byteCRS) Close() error { + return nil +} + +var _ crs = &byteCRS{} + +type httpFile struct { + File *File + crs +} + +func (h httpFile) Readdir(n int) ([]os.FileInfo, error) { + if h.File == nil { + return nil, os.ErrNotExist + } + return h.File.Readdir(n) +} + +func (h httpFile) Stat() (os.FileInfo, error) { + if h.File == nil { + return nil, os.ErrNotExist + } + return h.File.Stat() +} + +var _ http.File = &httpFile{} diff --git a/index.go b/index.go new file mode 100644 index 0000000..ca6acef --- /dev/null +++ b/index.go @@ -0,0 +1,116 @@ +package pkger + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/gobuffalo/here" + "github.com/markbates/pkger/pkgs" +) + +type index struct { + Pkg string + Files map[Path]*File +} + +func (i index) Walk(pt Path, wf WalkFunc) error { + + if len(pt.Pkg) == 0 { + pt.Pkg = i.Pkg + } + if len(i.Files) > 0 { + for k, v := range i.Files { + if k.Pkg != pt.Pkg { + continue + } + if err := wf(k, v.info, nil); err != nil { + return err + } + } + } + + var info here.Info + var err error + if pt.Pkg == "." { + info, err = pkgs.Current() + if err != nil { + return err + } + pt.Pkg = info.ImportPath + } + if info.IsZero() { + info, err = pkgs.Pkg(pt.Pkg) + if err != nil { + return fmt.Errorf("%s: %s", pt, err) + } + } + + err = filepath.Walk(info.Dir, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + path = strings.TrimPrefix(path, info.Dir) + pt, err := Parse(fmt.Sprintf("%s:%s", pt.Pkg, path)) + if err != nil { + return err + } + return wf(pt, NewFileInfo(fi), err) + }) + + return err +} + +func (i index) Open(pt Path) (*File, error) { + if len(pt.Pkg) == 0 { + pt.Pkg = i.Pkg + } + + f, ok := i.Files[pt] + if !ok { + return i.openDisk(pt) + } + return &File{ + info: f.info, + path: f.path, + data: f.data, + her: f.her, + index: &index{ + Files: map[Path]*File{}, + }, + }, nil +} + +func (i index) openDisk(pt Path) (*File, error) { + if len(pt.Pkg) == 0 { + pt.Pkg = i.Pkg + } + info, err := pkgs.Pkg(pt.Pkg) + if err != nil { + return nil, err + } + fp := info.Dir + if len(pt.Name) > 0 { + fp = filepath.Join(fp, pt.Name) + } + + fi, err := os.Stat(fp) + if err != nil { + return nil, err + } + f := &File{ + info: WithName(pt.Name, NewFileInfo(fi)), + her: info, + path: pt, + index: &index{ + Files: map[Path]*File{}, + }, + } + return f, nil +} + +var rootIndex = &index{ + Files: map[Path]*File{}, +} diff --git a/internal/examples/extfile/main.go b/internal/examples/extfile/main.go new file mode 100644 index 0000000..72482ce --- /dev/null +++ b/internal/examples/extfile/main.go @@ -0,0 +1,28 @@ +package main + +import ( + "fmt" + "io" + "log" + "os" + + "github.com/markbates/pkger" +) + +func main() { + f, err := pkger.Open("github.com/gobuffalo/buffalo:/server.go") + if err != nil { + log.Fatal("1", err) + } + + defer f.Close() + + fi, err := f.Stat() + if err != nil { + log.Fatal("2", err) + } + + fmt.Println(fi) + + io.Copy(os.Stdout, f) +} diff --git a/internal/examples/httpserver/main.go b/internal/examples/httpserver/main.go new file mode 100644 index 0000000..96eeef3 --- /dev/null +++ b/internal/examples/httpserver/main.go @@ -0,0 +1,43 @@ +package main + +import ( + "fmt" + "io" + "log" + "net/http" + "os" + "time" + + "github.com/markbates/pkger" +) + +func main() { + f, err := pkger.Open("github.com/gobuffalo/buffalo") + if err != nil { + log.Fatal("1", err) + } + defer f.Close() + + fmt.Println(f.Path()) + + go func() { + time.Sleep(1 * time.Second) + res, err := http.Get("http://127.0.0.1:3000/app.go") + if err != nil { + log.Fatal(err) + } + defer res.Body.Close() + + _, err = io.Copy(os.Stdout, res.Body) + if err != nil { + log.Fatal(err) + } + + if res.StatusCode >= 300 { + log.Fatal("code: ", res.StatusCode) + } + + }() + + log.Fatal(http.ListenAndServe(":3000", http.FileServer(f))) +} diff --git a/internal/examples/walk/main.go b/internal/examples/walk/main.go new file mode 100644 index 0000000..3caad92 --- /dev/null +++ b/internal/examples/walk/main.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/markbates/pkger" +) + +func main() { + err := pkger.Walk("github.com/gobuffalo/buffalo", func(path pkger.Path, info os.FileInfo, err error) error { + if err != nil { + return err + } + fmt.Println(path) + return nil + }) + if err != nil { + log.Fatal(err) + } +} diff --git a/path.go b/path.go new file mode 100644 index 0000000..ee09b66 --- /dev/null +++ b/path.go @@ -0,0 +1,43 @@ +package pkger + +import ( + "fmt" + "strings" +) + +type Path struct { + Pkg string + Name string +} + +func (p Path) String() string { + if len(p.Pkg) == 0 { + return p.Name + } + if len(p.Name) == 0 { + return p.Pkg + } + return fmt.Sprintf("%s:/%s", p.Pkg, p.Name) +} + +func Parse(p string) (Path, error) { + var pt Path + res := strings.Split(p, ":") + + if len(res) < 1 { + return pt, fmt.Errorf("could not parse %q (%d)", res, len(res)) + } + if len(res) == 1 { + if strings.HasPrefix(res[0], "/") { + pt.Name = res[0] + } else { + pt.Pkg = res[0] + } + } else { + pt.Pkg = res[0] + pt.Name = res[1] + } + pt.Name = strings.TrimPrefix(pt.Name, "/") + pt.Pkg = strings.TrimPrefix(pt.Pkg, "/") + return pt, nil +} diff --git a/pkger.go b/pkger.go new file mode 100644 index 0000000..aabc11b --- /dev/null +++ b/pkger.go @@ -0,0 +1,35 @@ +package pkger + +import ( + "bytes" + "fmt" + "os/exec" + "path/filepath" +) + +func modRoot() (string, error) { + c := exec.Command("go", "env", "GOMOD") + b, err := c.CombinedOutput() + if err != nil { + return "", err + } + + b = bytes.TrimSpace(b) + if len(b) == 0 { + return "", fmt.Errorf("the `go env GOMOD` was empty/modules are required") + } + + return filepath.Dir(string(b)), nil +} + +func Getwd() (string, error) { + return modRoot() +} + +func Open(p string) (*File, error) { + pt, err := Parse(p) + if err != nil { + return nil, err + } + return rootIndex.Open(pt) +} diff --git a/pkger/cmd/pkger/go.mod b/pkger/cmd/pkger/go.mod deleted file mode 100644 index c28bf27..0000000 --- a/pkger/cmd/pkger/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/markbates/pkger/pkger/cmd/pkger - -go 1.12 diff --git a/pkger/cmd/pkger/main.go b/pkger/cmd/pkger/main.go deleted file mode 100644 index 50e8d8d..0000000 --- a/pkger/cmd/pkger/main.go +++ /dev/null @@ -1,7 +0,0 @@ -package main - -import "fmt" - -func main() { - fmt.Println("vim-go") -} diff --git a/pkgs/current.go b/pkgs/current.go new file mode 100644 index 0000000..8b442b6 --- /dev/null +++ b/pkgs/current.go @@ -0,0 +1,28 @@ +package pkgs + +import ( + "os" + "path/filepath" + + "github.com/gobuffalo/here" +) + +func Pkg(p string) (here.Info, error) { + return here.Cache(p, here.Package) +} + +func Dir(p string) (here.Info, error) { + return here.Cache(p, here.Dir) +} + +func Current() (here.Info, error) { + return Dir(".") +} + +func Open(info here.Info, p string) (*os.File, error) { + return os.Open(filepath.Join(info.Dir, p)) +} + +func Stat(info here.Info, p string) (os.FileInfo, error) { + return os.Stat(filepath.Join(info.Dir, p)) +} diff --git a/walk.go b/walk.go new file mode 100644 index 0000000..58722d5 --- /dev/null +++ b/walk.go @@ -0,0 +1,13 @@ +package pkger + +import "os" + +type WalkFunc func(Path, os.FileInfo, error) error + +func Walk(p string, wf WalkFunc) error { + pt, err := Parse(p) + if err != nil { + return err + } + return rootIndex.Walk(pt, wf) +}