From b42e0b7d71b651fc50c4f1833cdb8e61008b008e Mon Sep 17 00:00:00 2001 From: Mark Bates Date: Sat, 31 Aug 2019 17:00:24 -0400 Subject: [PATCH] job jargon --- .gitignore | 1 + Makefile | 6 +- create_test.go | 4 +- fs/file.go | 22 +++++ fs/fs.go | 20 ++++ fs/fstest/file.go | 37 +++++++ fs/hdfs/file.go | 82 +++++++++++++++ fs/hdfs/hdfs.go | 153 ++++++++++++++++++++++++++++ fs/info.go | 104 +++++++++++++++++++ fs/memfs/create.go | 40 ++++++++ fs/memfs/create_test.go | 90 +++++++++++++++++ fs/memfs/file.go | 203 ++++++++++++++++++++++++++++++++++++++ fs/memfs/file_test.go | 70 +++++++++++++ fs/memfs/http.go | 1 + fs/memfs/http_test.go | 84 ++++++++++++++++ fs/memfs/json.go | 66 +++++++++++++ fs/memfs/json_test.go | 43 ++++++++ fs/memfs/memfs.go | 58 +++++++++++ fs/memfs/memfs_test.go | 26 +++++ fs/memfs/mkdirall.go | 61 ++++++++++++ fs/memfs/mkdirall_test.go | 25 +++++ fs/memfs/open.go | 32 ++++++ fs/memfs/open_test.go | 34 +++++++ fs/memfs/stat.go | 18 ++++ fs/memfs/stat_test.go | 25 +++++ fs/memfs/walk.go | 38 +++++++ fs/memfs/walk_test.go | 69 +++++++++++++ fs/path.go | 39 ++++++++ internal/maps/files.go | 146 +++++++++++++++++++++++++++ internal/maps/infos.go | 126 +++++++++++++++++++++++ internal/maps/paths.go | 182 ++++++++++++++++++++++++++++++++++ 31 files changed, 1902 insertions(+), 3 deletions(-) create mode 100644 fs/file.go create mode 100644 fs/fs.go create mode 100644 fs/fstest/file.go create mode 100644 fs/hdfs/file.go create mode 100644 fs/hdfs/hdfs.go create mode 100644 fs/info.go create mode 100644 fs/memfs/create.go create mode 100644 fs/memfs/create_test.go create mode 100644 fs/memfs/file.go create mode 100644 fs/memfs/file_test.go create mode 100644 fs/memfs/http.go create mode 100644 fs/memfs/http_test.go create mode 100644 fs/memfs/json.go create mode 100644 fs/memfs/json_test.go create mode 100644 fs/memfs/memfs.go create mode 100644 fs/memfs/memfs_test.go create mode 100644 fs/memfs/mkdirall.go create mode 100644 fs/memfs/mkdirall_test.go create mode 100644 fs/memfs/open.go create mode 100644 fs/memfs/open_test.go create mode 100644 fs/memfs/stat.go create mode 100644 fs/memfs/stat_test.go create mode 100644 fs/memfs/walk.go create mode 100644 fs/memfs/walk_test.go create mode 100644 fs/path.go create mode 100644 internal/maps/files.go create mode 100644 internal/maps/infos.go create mode 100644 internal/maps/paths.go diff --git a/.gitignore b/.gitignore index a10e98a..9115a58 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ bin/* gin-bin .idea/ pkged.go +cover.out diff --git a/Makefile b/Makefile index 7f64c7a..94018ac 100644 --- a/Makefile +++ b/Makefile @@ -16,6 +16,11 @@ test: tidy $(GO_BIN) test -cover -tags ${TAGS} -timeout 5s ./... make tidy +cov: + $(GO_BIN) test -coverprofile cover.out -tags ${TAGS} ./... + go tool cover -html cover.out + make tidy + ci-test: $(GO_BIN) test -tags ${TAGS} -race ./... @@ -43,4 +48,3 @@ release: make tidy - diff --git a/create_test.go b/create_test.go index 79caf2a..f53b023 100644 --- a/create_test.go +++ b/create_test.go @@ -9,7 +9,7 @@ import ( "github.com/stretchr/testify/require" ) -func Testg_Create(t *testing.T) { +func Test_Create(t *testing.T) { r := require.New(t) f, err := Create("/hello.txt") @@ -28,7 +28,7 @@ func Testg_Create(t *testing.T) { r.Equal("github.com/markbates/pkger", her.ImportPath) } -func Testg_Create_Write(t *testing.T) { +func Test_Create_Write(t *testing.T) { r := require.New(t) f, err := Create("/hello.txt") diff --git a/fs/file.go b/fs/file.go new file mode 100644 index 0000000..d558b7b --- /dev/null +++ b/fs/file.go @@ -0,0 +1,22 @@ +package fs + +import ( + "net/http" + "os" + + "github.com/markbates/pkger/here" +) + +type File interface { + Close() error + FilePath() string + Info() here.Info + Name() string + Open(name string) (http.File, error) + Path() Path + Read(p []byte) (int, error) + Readdir(count int) ([]os.FileInfo, error) + Seek(offset int64, whence int) (int64, error) + Stat() (os.FileInfo, error) + Write(b []byte) (int, error) +} diff --git a/fs/fs.go b/fs/fs.go new file mode 100644 index 0000000..99482a3 --- /dev/null +++ b/fs/fs.go @@ -0,0 +1,20 @@ +package fs + +import ( + "os" + "path/filepath" + + "github.com/markbates/pkger/here" +) + +type FileSystem interface { + Create(name string) (File, error) + Current() (here.Info, error) + Info(p string) (here.Info, error) + MkdirAll(p string, perm os.FileMode) error + Open(name string) (File, error) + Parse(p string) (Path, error) + ReadFile(s string) ([]byte, error) + Stat(name string) (os.FileInfo, error) + Walk(p string, wf filepath.WalkFunc) error +} diff --git a/fs/fstest/file.go b/fs/fstest/file.go new file mode 100644 index 0000000..cc675df --- /dev/null +++ b/fs/fstest/file.go @@ -0,0 +1,37 @@ +package fstest + +import ( + "bytes" + "io" + + "github.com/markbates/pkger/fs" +) + +type TestFile struct { + Name string + Data []byte +} + +func (t TestFile) Create(fx fs.FileSystem) error { + f, err := fx.Create(t.Name) + if err != nil { + return err + } + _, err = io.Copy(f, bytes.NewReader(t.Data)) + if err != nil { + return err + } + return f.Close() +} + +type TestFiles map[string]TestFile + +func (t TestFiles) Create(fx fs.FileSystem) error { + for k, f := range t { + f.Name = k + if err := f.Create(fx); err != nil { + return err + } + } + return nil +} diff --git a/fs/hdfs/file.go b/fs/hdfs/file.go new file mode 100644 index 0000000..572e4cd --- /dev/null +++ b/fs/hdfs/file.go @@ -0,0 +1,82 @@ +package hdfs + +import ( + "net/http" + "os" + + "github.com/markbates/pkger/fs" + "github.com/markbates/pkger/here" +) + +var _ fs.File = &File{} + +type File struct { + *os.File + filePath string + info *fs.FileInfo + her here.Info + path fs.Path + fs fs.FileSystem +} + +func NewFile(fx fs.FileSystem, osf *os.File) (*File, error) { + info, err := osf.Stat() + if err != nil { + return nil, err + } + + pt, err := fx.Parse(info.Name()) + if err != nil { + return nil, err + } + + f := &File{ + File: osf, + filePath: info.Name(), + path: pt, + fs: fx, + } + f.info = fs.WithName(pt.Name, info) + + her, err := here.Package(pt.Pkg) + if err != nil { + return nil, err + } + f.her = her + return f, nil +} + +func (f *File) Close() error { + return f.File.Close() +} + +func (f *File) FilePath() string { + return f.filePath +} + +func (f *File) Info() here.Info { + return f.her +} + +func (f *File) Name() string { + return f.info.Name() +} + +func (f *File) Open(name string) (http.File, error) { + return f.File, nil +} + +func (f *File) Path() fs.Path { + return f.path +} + +func (f *File) Stat() (os.FileInfo, error) { + if f.info == nil { + info, err := os.Stat(f.filePath) + if err != nil { + return nil, err + } + f.info = fs.NewFileInfo(info) + } + return f.info, nil +} diff --git a/fs/hdfs/hdfs.go b/fs/hdfs/hdfs.go new file mode 100644 index 0000000..9d05773 --- /dev/null +++ b/fs/hdfs/hdfs.go @@ -0,0 +1,153 @@ +package hdfs + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/markbates/pkger/fs" + "github.com/markbates/pkger/here" + "github.com/markbates/pkger/internal/maps" +) + +var _ fs.FileSystem = &FS{} + +type FS struct { + infos *maps.Infos + paths *maps.Paths + current here.Info +} + +func New() (*FS, error) { + f := &FS{ + infos: &maps.Infos{}, + paths: &maps.Paths{}, + } + + var err error + f.current, err = here.Current() + return f, err +} + +func (fx *FS) Create(name string) (fs.File, error) { + name, err := fx.locate(name) + if err != nil { + return nil, err + } + f, err := os.Create(name) + if err != nil { + return nil, err + } + return NewFile(fx, f) +} + +func (f *FS) Current() (here.Info, error) { + return f.current, nil +} + +func (f *FS) Info(p string) (here.Info, error) { + info, ok := f.infos.Load(p) + if ok { + return info, nil + } + + info, err := here.Package(p) + if err != nil { + return info, err + } + f.infos.Store(p, info) + return info, nil +} + +func (f *FS) MkdirAll(p string, perm os.FileMode) error { + p, err := f.locate(p) + if err != nil { + return err + } + return os.MkdirAll(p, perm) +} + +func (fx *FS) Open(name string) (fs.File, error) { + name, err := fx.locate(name) + if err != nil { + return nil, err + } + f, err := os.Open(name) + if err != nil { + return nil, err + } + return NewFile(fx, f) +} + +func (f *FS) Parse(p string) (fs.Path, error) { + return f.paths.Parse(p) +} + +func (f *FS) ReadFile(s string) ([]byte, error) { + s, err := f.locate(s) + if err != nil { + return nil, err + } + return ioutil.ReadFile(s) +} + +func (f *FS) Stat(name string) (os.FileInfo, error) { + name, err := f.locate(name) + if err != nil { + return nil, err + } + return os.Stat(name) +} + +func (f *FS) Walk(p string, wf filepath.WalkFunc) error { + fp, err := f.locate(p) + if err != nil { + return err + } + + pt, err := f.Parse(p) + if err != nil { + return err + } + err = filepath.Walk(fp, func(path string, fi os.FileInfo, err error) error { + if err != nil { + return err + } + + path = strings.TrimPrefix(path, fp) + pt, err := f.Parse(fmt.Sprintf("%s:%s", pt.Pkg, path)) + if err != nil { + return err + } + return wf(pt.String(), fs.WithName(path, fs.NewFileInfo(fi)), nil) + }) + + return err +} + +func (f *FS) locate(p string) (string, error) { + pt, err := f.Parse(p) + if err != nil { + return "", err + } + + var info here.Info + if pt.Pkg == "." { + info, err = f.Current() + if err != nil { + return "", err + } + pt.Pkg = info.ImportPath + } + + if info.IsZero() { + info, err = f.Info(pt.Pkg) + if err != nil { + return "", fmt.Errorf("%s: %s", pt, err) + } + } + fp := filepath.Join(info.Dir, pt.Name) + return fp, nil +} diff --git a/fs/info.go b/fs/info.go new file mode 100644 index 0000000..75721db --- /dev/null +++ b/fs/info.go @@ -0,0 +1,104 @@ +package fs + +import ( + "encoding/json" + "os" + "strings" + "time" +) + +const timeFmt = time.RFC3339Nano + +type ModTime time.Time + +func (m ModTime) MarshalJSON() ([]byte, error) { + t := time.Time(m) + return json.Marshal(t.Format(timeFmt)) +} + +func (m *ModTime) UnmarshalJSON(b []byte) error { + t := time.Time{} + if err := json.Unmarshal(b, &t); err != nil { + return err + } + (*m) = ModTime(t) + return nil +} + +type Details struct { + Name string `json:"name"` + Size int64 `json:"size"` + Mode os.FileMode `json:"mode"` + ModTime ModTime `json:"mod_time"` + IsDir bool `json:"is_dir"` + Sys interface{} `json:"sys"` +} +type FileInfo struct { + Details `json:"details"` +} + +func (f *FileInfo) String() string { + b, _ := json.MarshalIndent(f, "", " ") + return string(b) +} + +func (f *FileInfo) Name() string { + return f.Details.Name +} + +func (f *FileInfo) Size() int64 { + return f.Details.Size +} + +func (f *FileInfo) Mode() os.FileMode { + return f.Details.Mode +} + +func (f *FileInfo) ModTime() time.Time { + return time.Time(f.Details.ModTime) +} + +func (f *FileInfo) IsDir() bool { + return f.Details.IsDir +} + +func (f *FileInfo) Sys() interface{} { + return f.Details.Sys +} + +var _ os.FileInfo = &FileInfo{} + +func NewFileInfo(info os.FileInfo) *FileInfo { + fi := &FileInfo{ + Details: Details{ + Name: cleanName(info.Name()), + Size: info.Size(), + Mode: info.Mode(), + ModTime: 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.Details.Name = cleanName(name) + return ft + } + + fo := NewFileInfo(info) + fo.Details.Name = cleanName(name) + return fo +} + +func cleanName(s string) string { + if strings.Contains(s, "\\") { + s = strings.Replace(s, "\\", "/", -1) + } + if !strings.HasPrefix(s, "/") { + s = "/" + s + } + return s +} diff --git a/fs/memfs/create.go b/fs/memfs/create.go new file mode 100644 index 0000000..76bd253 --- /dev/null +++ b/fs/memfs/create.go @@ -0,0 +1,40 @@ +package memfs + +import ( + "path/filepath" + "time" + + "github.com/markbates/pkger/fs" +) + +func (fx *FS) Create(name string) (fs.File, error) { + pt, err := fx.Parse(name) + if err != nil { + return nil, err + } + + her, err := fx.Info(pt.Pkg) + if err != nil { + return nil, err + } + f := &File{ + path: pt, + her: her, + info: &fs.FileInfo{ + Details: fs.Details{ + Name: pt.Name, + Mode: 0666, + ModTime: fs.ModTime(time.Now()), + }, + }, + fs: fx, + } + + fx.files.Store(pt, f) + + dir := filepath.Dir(pt.Name) + if err := fx.MkdirAll(dir, 0644); err != nil { + return nil, err + } + return f, nil +} diff --git a/fs/memfs/create_test.go b/fs/memfs/create_test.go new file mode 100644 index 0000000..411dbca --- /dev/null +++ b/fs/memfs/create_test.go @@ -0,0 +1,90 @@ +package memfs + +import ( + "io" + "os" + "strings" + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_Create(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + f, err := fs.Create("/hello.txt") + r.NoError(err) + r.NotNil(f) + + fi, err := f.Stat() + r.NoError(err) + + r.Equal("/hello.txt", fi.Name()) + r.Equal(os.FileMode(0666), fi.Mode()) + r.NotZero(fi.ModTime()) +} + +func Test_Create_Write(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + f, err := fs.Create("/hello.txt") + r.NoError(err) + r.NotNil(f) + + fi, err := f.Stat() + r.NoError(err) + r.Zero(fi.Size()) + + r.Equal("/hello.txt", fi.Name()) + + mt := fi.ModTime() + r.NotZero(mt) + + sz, err := io.Copy(f, strings.NewReader(radio)) + r.NoError(err) + r.Equal(int64(1381), sz) + + r.NoError(f.Close()) + r.Equal(int64(1381), fi.Size()) + r.NotZero(fi.ModTime()) + r.NotEqual(mt, fi.ModTime()) +} + +const radio = `I was tuning in the shine on the late night dial +Doing anything my radio advised +With every one of those late night stations +Playing songs bringing tears to my eyes +I was seriously thinking about hiding the receiver +When the switch broke 'cause it's old +They're saying things that I can hardly believe +They really think we're getting out of control +Radio is a sound salvation +Radio is cleaning up the nation +They say you better listen to the voice of reason +But they don't give you any choice 'cause they think that it's treason +So you had better do as you are told +You better listen to the radio +I wanna bite the hand that feeds me +I wanna bite that hand so badly +I want to make them wish they'd never seen me +Some of my friends sit around every evening +And they worry about the times ahead +But everybody else is overwhelmed by indifference +And the promise of an early bed +You either shut up or get cut up; they don't wanna hear about it +It's only inches on the reel-to-reel +And the radio is in the hands of such a lot of fools +Tryin' to anesthetize the way that you feel +Radio is a sound salvation +Radio is cleaning up the nation +They say you better listen to the voice of reason +But they don't give you any choice 'cause they think that it's treason +So you had better do as you are told +You better listen to the radio +Wonderful radio +Marvelous radio +Wonderful radio +Radio, radio` diff --git a/fs/memfs/file.go b/fs/memfs/file.go new file mode 100644 index 0000000..612c12b --- /dev/null +++ b/fs/memfs/file.go @@ -0,0 +1,203 @@ +package memfs + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "path" + "strings" + "time" + + "github.com/markbates/pkger/fs" + "github.com/markbates/pkger/here" +) + +const timeFmt = time.RFC3339Nano + +var _ fs.File = &File{} + +type File struct { + info *fs.FileInfo + her here.Info + path fs.Path + data []byte + parent fs.Path + writer *bytes.Buffer + reader io.Reader + fs fs.FileSystem +} + +func (f *File) Seek(offset int64, whence int) (int64, error) { + if sk, ok := f.reader.(io.Seeker); ok { + return sk.Seek(offset, whence) + } + return 0, nil +} + +func (f *File) Close() error { + defer func() { + f.reader = nil + f.writer = nil + }() + if f.reader != nil { + if c, ok := f.reader.(io.Closer); ok { + if err := c.Close(); err != nil { + return err + } + } + } + + if f.writer == nil { + return nil + } + + f.data = f.writer.Bytes() + + fi := f.info + fi.Details.Size = int64(len(f.data)) + fi.Details.ModTime = fs.ModTime(time.Now()) + f.info = fi + return nil +} + +func (f *File) Read(p []byte) (int, error) { + if len(f.data) > 0 && f.reader == nil { + f.reader = bytes.NewReader(f.data) + } + + if f.reader != nil { + return f.reader.Read(p) + } + + return 0, fmt.Errorf("unable to read %s", f.Name()) +} + +func (f *File) Write(b []byte) (int, error) { + if f.writer == nil { + f.writer = &bytes.Buffer{} + } + i, err := f.writer.Write(b) + return i, err +} + +func (f File) Info() here.Info { + return f.her +} + +func (f File) Stat() (os.FileInfo, error) { + if f.info == nil { + return nil, os.ErrNotExist + } + return f.info, nil +} + +func (f File) Name() string { + return f.info.Name() +} + +func (f File) FilePath() string { + return f.her.FilePath(f.Name()) +} + +func (f File) Path() fs.Path { + return f.path +} + +func (f File) String() string { + return f.Path().String() +} + +func (f File) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + if st.Flag('+') { + b, err := json.MarshalIndent(f, "", " ") + if err != nil { + fmt.Fprint(os.Stderr, err) + return + } + fmt.Fprint(st, string(b)) + return + } + fmt.Fprint(st, f.String()) + case 'q': + fmt.Fprintf(st, "%q", f.String()) + default: + fmt.Fprint(st, f.String()) + } +} + +func (f *File) Readdir(count int) ([]os.FileInfo, error) { + var infos []os.FileInfo + root := f.Path().String() + err := f.fs.Walk(root, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if count > 0 && len(infos) == count { + return io.EOF + } + + pt, err := f.fs.Parse(path) + if err != nil { + return err + } + if pt.Name == f.parent.Name { + return nil + } + // if f.parent.Name != "/" { + info = fs.WithName(strings.TrimPrefix(info.Name(), f.parent.Name), info) + // } + infos = append(infos, info) + return nil + }) + + if err != nil { + if _, ok := err.(*os.PathError); ok { + return infos, nil + } + if err != io.EOF { + return nil, err + } + } + return infos, nil + +} + +func (f *File) Open(name string) (http.File, error) { + pt, err := f.fs.Parse(name) + if err != nil { + return nil, err + } + + if pt == f.path { + return f, nil + } + + pt.Name = path.Join(f.Path().Name, pt.Name) + + di, err := f.fs.Open(pt.String()) + if err != nil { + return nil, err + } + + fi, err := di.Stat() + if err != nil { + return nil, err + } + if fi.IsDir() { + d2 := &File{ + info: fs.NewFileInfo(fi), + her: di.Info(), + path: pt, + parent: f.path, + fs: f.fs, + } + di = d2 + } + return di, nil +} diff --git a/fs/memfs/file_test.go b/fs/memfs/file_test.go new file mode 100644 index 0000000..73e9552 --- /dev/null +++ b/fs/memfs/file_test.go @@ -0,0 +1,70 @@ +package memfs + +import ( + "bytes" + "io" + "io/ioutil" + "strings" + "testing" + "time" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_File_Read_Memory(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + r.NoError(err) + + f, err := fs.Create("/file_test.go") + r.NoError(err) + _, err = io.Copy(f, bytes.NewReader([]byte("hi!"))) + r.NoError(err) + r.NoError(f.Close()) + + f, err = fs.Open("/file_test.go") + r.NoError(err) + fi, err := f.Stat() + r.NoError(err) + r.Equal("/file_test.go", fi.Name()) + + b, err := ioutil.ReadAll(f) + r.NoError(err) + r.Equal(string(b), "hi!") +} + +func Test_File_Write(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + r.NoError(err) + + f, err := fs.Create("/hello.txt") + r.NoError(err) + r.NotNil(f) + + fi, err := f.Stat() + r.NoError(err) + r.Zero(fi.Size()) + + r.Equal("/hello.txt", fi.Name()) + + mt := fi.ModTime() + r.NotZero(mt) + + sz, err := io.Copy(f, strings.NewReader(radio)) + r.NoError(err) + r.Equal(int64(1381), sz) + + // because windows can't handle the time precisely + // enough, we have to *force* just a smidge of time + // to ensure the two ModTime's are different. + // i know, i hate it too. + time.Sleep(time.Millisecond) + r.NoError(f.Close()) + r.Equal(int64(1381), fi.Size()) + r.NotZero(fi.ModTime()) + r.NotEqual(mt, fi.ModTime()) +} diff --git a/fs/memfs/http.go b/fs/memfs/http.go new file mode 100644 index 0000000..8151e03 --- /dev/null +++ b/fs/memfs/http.go @@ -0,0 +1 @@ +package memfs diff --git a/fs/memfs/http_test.go b/fs/memfs/http_test.go new file mode 100644 index 0000000..1dbf8b6 --- /dev/null +++ b/fs/memfs/http_test.go @@ -0,0 +1,84 @@ +package memfs + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_HTTP_Dir(t *testing.T) { + r := require.New(t) + + fs := NewFS() + + r.NoError(Folder.Create(fs)) + + dir, err := fs.Open("/") + r.NoError(err) + ts := httptest.NewServer(http.FileServer(dir)) + defer ts.Close() + + res, err := http.Get(ts.URL + "/") + r.NoError(err) + r.Equal(200, res.StatusCode) + + b, err := ioutil.ReadAll(res.Body) + r.NoError(err) + r.Contains(string(b), `/public/images/mark.png`) +} + +func Test_HTTP_File_Memory(t *testing.T) { + r := require.New(t) + + fs := NewFS() + r.NoError(Folder.Create(fs)) + + dir, err := fs.Open("/") + r.NoError(err) + ts := httptest.NewServer(http.FileServer(dir)) + defer ts.Close() + + res, err := http.Get(ts.URL + "/public/images/mark.png") + r.NoError(err) + r.Equal(200, res.StatusCode) + + b, err := ioutil.ReadAll(res.Body) + r.NoError(err) + r.Contains(string(b), `!/public/images/mark.png`) +} + +func Test_HTTP_Dir_Memory_StripPrefix(t *testing.T) { + r := require.New(t) + + fs := NewFS() + r.NoError(Folder.Create(fs)) + + dir, err := fs.Open("/public") + r.NoError(err) + defer dir.Close() + + ts := httptest.NewServer(http.StripPrefix("/assets/", http.FileServer(dir))) + defer ts.Close() + + res, err := http.Get(ts.URL + "/assets/images/mark.png") + r.NoError(err) + r.Equal(200, res.StatusCode) + + b, _ := ioutil.ReadAll(res.Body) + // r.NoError(err) + r.Contains(string(b), "!/public/images/mark.png") + + res, err = http.Get(ts.URL + "/assets/images/") + r.NoError(err) + r.Equal(200, res.StatusCode) + + b, _ = ioutil.ReadAll(res.Body) + // r.NoError(err) + r.Contains(string(b), `/mark.png`) + r.NotContains(string(b), `/public`) + r.NotContains(string(b), `/images`) + r.NotContains(string(b), `/go.mod`) +} diff --git a/fs/memfs/json.go b/fs/memfs/json.go new file mode 100644 index 0000000..31bb74d --- /dev/null +++ b/fs/memfs/json.go @@ -0,0 +1,66 @@ +package memfs + +import ( + "encoding/json" + "fmt" + + "github.com/markbates/pkger/fs" +) + +func (f File) MarshalJSON() ([]byte, error) { + m := map[string]interface{}{ + "info": f.info, + "her": f.her, + "path": f.path, + "data": f.data, + "parent": f.parent, + } + return json.Marshal(m) +} + +func (f *File) UnmarshalJSON(b []byte) error { + m := map[string]json.RawMessage{} + if err := json.Unmarshal(b, &m); err != nil { + return err + } + + info, ok := m["info"] + if !ok { + return fmt.Errorf("missing info") + } + + f.info = &fs.FileInfo{} + if err := json.Unmarshal(info, f.info); err != nil { + return err + } + + her, ok := m["her"] + if !ok { + return fmt.Errorf("missing her") + } + if err := json.Unmarshal(her, &f.her); err != nil { + return err + } + + path, ok := m["path"] + if !ok { + return fmt.Errorf("missing path") + } + if err := json.Unmarshal(path, &f.path); err != nil { + return err + } + + parent, ok := m["parent"] + if !ok { + return fmt.Errorf("missing parent") + } + if err := json.Unmarshal(parent, &f.parent); err != nil { + return err + } + + if err := json.Unmarshal(m["data"], &f.data); err != nil { + return err + } + + return nil +} diff --git a/fs/memfs/json_test.go b/fs/memfs/json_test.go new file mode 100644 index 0000000..9ba7487 --- /dev/null +++ b/fs/memfs/json_test.go @@ -0,0 +1,43 @@ +package memfs + +import ( + "encoding/json" + "io" + "strings" + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_File_JSON(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + r.NoError(err) + + f, err := fs.Create("/radio.radio") + r.NoError(err) + _, err = io.Copy(f, strings.NewReader(radio)) + r.NoError(err) + r.NoError(f.Close()) + + f, err = fs.Open("/radio.radio") + r.NoError(err) + bi, err := f.Stat() + r.NoError(err) + + mj, err := json.Marshal(f) + r.NoError(err) + + f2 := &File{} + + r.NoError(json.Unmarshal(mj, f2)) + + ai, err := f2.Stat() + r.NoError(err) + + r.Equal(bi.Size(), ai.Size()) + + r.Equal(radio, string(f2.data)) +} diff --git a/fs/memfs/memfs.go b/fs/memfs/memfs.go new file mode 100644 index 0000000..f88065b --- /dev/null +++ b/fs/memfs/memfs.go @@ -0,0 +1,58 @@ +package memfs + +import ( + "io/ioutil" + + "github.com/markbates/pkger/fs" + "github.com/markbates/pkger/here" + "github.com/markbates/pkger/internal/maps" +) + +var _ fs.FileSystem = &FS{} + +func New(info here.Info) (*FS, error) { + f := &FS{ + infos: &maps.Infos{}, + paths: &maps.Paths{}, + files: &maps.Files{}, + } + return f, nil +} + +type FS struct { + infos *maps.Infos + paths *maps.Paths + files *maps.Files + current here.Info +} + +func (f *FS) Current() (here.Info, error) { + return f.current, nil +} + +func (f *FS) Info(p string) (here.Info, error) { + info, ok := f.infos.Load(p) + if ok { + return info, nil + } + + info, err := here.Package(p) + if err != nil { + return info, err + } + f.infos.Store(p, info) + return info, nil +} + +func (f *FS) Parse(p string) (fs.Path, error) { + return f.paths.Parse(p) +} + +func (fx *FS) ReadFile(s string) ([]byte, error) { + f, err := fx.Open(s) + if err != nil { + return nil, err + } + defer f.Close() + return ioutil.ReadAll(f) +} diff --git a/fs/memfs/memfs_test.go b/fs/memfs/memfs_test.go new file mode 100644 index 0000000..fbfc5e3 --- /dev/null +++ b/fs/memfs/memfs_test.go @@ -0,0 +1,26 @@ +package memfs + +import ( + "log" + + "github.com/markbates/pkger/fs/fstest" + "github.com/markbates/pkger/here" +) + +func NewFS() *FS { + fs, err := New(here.Info{}) + if err != nil { + log.Fatal(err) + } + return fs +} + +var Folder = fstest.TestFiles{ + "/main.go": {Data: []byte("!/main.go")}, + "/go.mod": {Data: []byte("!/go.mod")}, + "/go.sum": {Data: []byte("!/go.sum")}, + "/public/index.html": {Data: []byte("!/public/index.html")}, + "/public/images/mark.png": {Data: []byte("!/public/images/mark.png")}, + "/templates/a.txt": {Data: []byte("!/templates/a.txt")}, + "/templates/b/b.txt": {Data: []byte("!/templates/b/b.txt")}, +} diff --git a/fs/memfs/mkdirall.go b/fs/memfs/mkdirall.go new file mode 100644 index 0000000..1cd5c43 --- /dev/null +++ b/fs/memfs/mkdirall.go @@ -0,0 +1,61 @@ +package memfs + +import ( + "os" + "path/filepath" + "time" + + "github.com/markbates/pkger/fs" +) + +func (fx *FS) MkdirAll(p string, perm os.FileMode) error { + path, err := fx.Parse(p) + if err != nil { + return err + } + root := path.Name + + cur, err := fx.Current() + if err != nil { + return err + } + for root != "" { + pt := fs.Path{ + Pkg: path.Pkg, + Name: root, + } + if _, ok := fx.files.Load(pt); ok { + root = filepath.Dir(root) + if root == "/" || root == "\\" { + break + } + continue + } + f := &File{ + fs: fx, + path: pt, + her: cur, + info: &fs.FileInfo{ + Details: fs.Details{ + Name: pt.Name, + Mode: perm, + ModTime: fs.ModTime(time.Now()), + }, + }, + } + + if err != nil { + return err + } + f.info.Details.IsDir = true + f.info.Details.Mode = perm + if err := f.Close(); err != nil { + return err + } + fx.files.Store(pt, f) + root = filepath.Dir(root) + } + + return nil + +} diff --git a/fs/memfs/mkdirall_test.go b/fs/memfs/mkdirall_test.go new file mode 100644 index 0000000..a9da153 --- /dev/null +++ b/fs/memfs/mkdirall_test.go @@ -0,0 +1,25 @@ +package memfs + +import ( + "os" + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_MkdirAll(t *testing.T) { + r := require.New(t) + + fs, _ := New(here.Info{}) + + err := fs.MkdirAll("/foo/bar/baz", 0755) + r.NoError(err) + + fi, err := fs.Stat("/foo/bar/baz") + r.NoError(err) + + r.Equal("/foo/bar/baz", fi.Name()) + r.Equal(os.FileMode(0755), fi.Mode()) + r.True(fi.IsDir()) +} diff --git a/fs/memfs/open.go b/fs/memfs/open.go new file mode 100644 index 0000000..78f27d6 --- /dev/null +++ b/fs/memfs/open.go @@ -0,0 +1,32 @@ +package memfs + +import ( + "fmt" + + "github.com/markbates/pkger/fs" +) + +func (fx *FS) Open(name string) (fs.File, error) { + pt, err := fx.Parse(name) + if err != nil { + return nil, err + } + + fl, ok := fx.files.Load(pt) + if !ok { + return nil, fmt.Errorf("could not open %s", name) + } + f, ok := fl.(*File) + if !ok { + return nil, fmt.Errorf("could not open %s", name) + } + nf := &File{ + fs: fx, + info: fs.WithName(f.info.Name(), f.info), + path: f.path, + data: f.data, + her: f.her, + } + + return nf, nil +} diff --git a/fs/memfs/open_test.go b/fs/memfs/open_test.go new file mode 100644 index 0000000..b173b08 --- /dev/null +++ b/fs/memfs/open_test.go @@ -0,0 +1,34 @@ +package memfs + +import ( + "io" + "io/ioutil" + "strings" + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_Open(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + r.NoError(err) + + _, err = fs.Open("/i.dont.exist") + r.Error(err) + + f, err := fs.Create("/i.exist") + r.NoError(err) + _, err = io.Copy(f, strings.NewReader(radio)) + r.NoError(err) + r.NoError(f.Close()) + + f, err = fs.Open("/i.exist") + r.NoError(err) + b, err := ioutil.ReadAll(f) + r.NoError(err) + r.NoError(f.Close()) + r.Equal([]byte(radio), b) +} diff --git a/fs/memfs/stat.go b/fs/memfs/stat.go new file mode 100644 index 0000000..67f702f --- /dev/null +++ b/fs/memfs/stat.go @@ -0,0 +1,18 @@ +package memfs + +import ( + "fmt" + "os" +) + +func (fx *FS) Stat(name string) (os.FileInfo, error) { + pt, err := fx.Parse(name) + if err != nil { + return nil, err + } + f, ok := fx.files.Load(pt) + if ok { + return f.Stat() + } + return nil, fmt.Errorf("could not stat %s", name) +} diff --git a/fs/memfs/stat_test.go b/fs/memfs/stat_test.go new file mode 100644 index 0000000..d6aa990 --- /dev/null +++ b/fs/memfs/stat_test.go @@ -0,0 +1,25 @@ +package memfs + +import ( + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_Stat(t *testing.T) { + r := require.New(t) + + fs, err := New(here.Info{}) + r.NoError(err) + _, err = fs.Stat("/i.dont.exist") + r.Error(err) + + f, err := fs.Create("/i.exist") + r.NoError(err) + r.NoError(f.Close()) + + fi, err := fs.Stat("/i.exist") + r.NoError(err) + r.Equal("/i.exist", fi.Name()) +} diff --git a/fs/memfs/walk.go b/fs/memfs/walk.go new file mode 100644 index 0000000..015acd5 --- /dev/null +++ b/fs/memfs/walk.go @@ -0,0 +1,38 @@ +package memfs + +import ( + "fmt" + "path/filepath" + "strings" + + "github.com/markbates/pkger/fs" +) + +func (f *FS) Walk(p string, wf filepath.WalkFunc) error { + keys := f.files.Keys() + + pt, err := f.Parse(p) + if err != nil { + return err + } + for _, k := range keys { + if !strings.HasPrefix(k.Name, pt.Name) { + continue + } + fl, ok := f.files.Load(k) + if !ok { + return fmt.Errorf("could not find %s", k) + } + fi, err := fl.Stat() + if err != nil { + return err + } + + fi = fs.WithName(strings.TrimPrefix(k.Name, pt.Name), fi) + err = wf(k.String(), fi, nil) + if err != nil { + return err + } + } + return nil +} diff --git a/fs/memfs/walk_test.go b/fs/memfs/walk_test.go new file mode 100644 index 0000000..aa56657 --- /dev/null +++ b/fs/memfs/walk_test.go @@ -0,0 +1,69 @@ +package memfs + +import ( + "io" + "os" + "sort" + "strings" + "testing" + + "github.com/markbates/pkger/here" + "github.com/stretchr/testify/require" +) + +func Test_Walk(t *testing.T) { + r := require.New(t) + + files := []struct { + name string + body string + }{ + {name: "/a/a.txt", body: "A"}, + {name: "/a/a.md", body: "Amd"}, + {name: "/b/c/d.txt", body: "B"}, + {name: "/f.txt", body: "F"}, + } + + sort.Slice(files, func(a, b int) bool { + return files[a].name < files[b].name + }) + + fs, err := New(here.Info{}) + r.NoError(err) + + for _, file := range files { + f, err := fs.Create(file.name) + r.NoError(err) + _, err = io.Copy(f, strings.NewReader(file.body)) + r.NoError(err) + r.NoError(f.Close()) + } + + var found []string + err = fs.Walk("/", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + found = append(found, path) + return nil + }) + r.NoError(err) + + expected := []string{":/", ":/a", ":/a/a.md", ":/a/a.txt", ":/b", ":/b/c", ":/b/c/d.txt", ":/f.txt"} + r.Equal(expected, found) + + found = []string{} + err = fs.Walk("/a/", func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + found = append(found, path) + return nil + }) + r.NoError(err) + + expected = []string{":/a/a.md", ":/a/a.txt"} + r.Equal(expected, found) +} diff --git a/fs/path.go b/fs/path.go new file mode 100644 index 0000000..902c31e --- /dev/null +++ b/fs/path.go @@ -0,0 +1,39 @@ +package fs + +import ( + "encoding/json" + "fmt" + "os" +) + +type Path struct { + Pkg string `json:"pkg"` + Name string `json:"name"` +} + +func (p Path) String() string { + if p.Name == "" { + p.Name = "/" + } + return fmt.Sprintf("%s:%s", p.Pkg, p.Name) +} + +func (p Path) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + if st.Flag('+') { + b, err := json.MarshalIndent(p, "", " ") + if err != nil { + fmt.Fprint(os.Stderr, err) + return + } + fmt.Fprint(st, string(b)) + return + } + fmt.Fprint(st, p.String()) + case 'q': + fmt.Fprintf(st, "%q", p.String()) + default: + fmt.Fprint(st, p.String()) + } +} diff --git a/internal/maps/files.go b/internal/maps/files.go new file mode 100644 index 0000000..c30170f --- /dev/null +++ b/internal/maps/files.go @@ -0,0 +1,146 @@ +// Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. + +package maps + +import ( + "encoding/json" + "fmt" + "sort" + "sync" + + "github.com/markbates/pkger/fs" +) + +// Files wraps sync.Map and uses the following types: +// key: fs.Path +// value: fs.File +type Files struct { + data *sync.Map + once *sync.Once +} + +func (m *Files) Data() *sync.Map { + if m.once == nil { + m.once = &sync.Once{} + } + m.once.Do(func() { + if m.data == nil { + m.data = &sync.Map{} + } + }) + return m.data +} + +func (m *Files) MarshalJSON() ([]byte, error) { + var err error + mm := map[string]interface{}{} + m.Data().Range(func(key, value interface{}) bool { + var b []byte + b, err = json.Marshal(key) + if err != nil { + return false + } + mm[string(b)] = value + return true + }) + + if err != nil { + return nil, err + } + + return json.Marshal(mm) +} + +func (m *Files) UnmarshalJSON(b []byte) error { + mm := map[string]fs.File{} + + if err := json.Unmarshal(b, &mm); err != nil { + return err + } + for k, v := range mm { + var pt fs.Path + if err := json.Unmarshal([]byte(k), &pt); err != nil { + return err + } + m.Store(pt, v) + } + return nil +} + +// Delete the key from the map +func (m *Files) Delete(key fs.Path) { + m.Data().Delete(key) +} + +// Load the key from the map. +// Returns fs.File or bool. +// A false return indicates either the key was not found +// or the value is not of type fs.File +func (m *Files) Load(key fs.Path) (fs.File, bool) { + i, ok := m.Data().Load(key) + if !ok { + return nil, false + } + s, ok := i.(fs.File) + return s, ok +} + +// LoadOrStore will return an existing key or +// store the value if not already in the map +func (m *Files) LoadOrStore(key fs.Path, value fs.File) (fs.File, bool) { + i, _ := m.Data().LoadOrStore(key, value) + s, ok := i.(fs.File) + return s, ok +} + +// LoadOr will return an existing key or +// run the function and store the results +func (m *Files) LoadOr(key fs.Path, fn func(*Files) (fs.File, bool)) (fs.File, bool) { + i, ok := m.Load(key) + if ok { + return i, ok + } + i, ok = fn(m) + if ok { + m.Store(key, i) + return i, ok + } + return i, false +} + +// Range over the fs.File values in the map +func (m *Files) Range(f func(key fs.Path, value fs.File) bool) { + m.Data().Range(func(k, v interface{}) bool { + key, ok := k.(fs.Path) + if !ok { + return false + } + value, ok := v.(fs.File) + if !ok { + return false + } + return f(key, value) + }) +} + +// Store a fs.File in the map +func (m *Files) Store(key fs.Path, value fs.File) { + m.Data().Store(key, value) +} + +// Keys returns a list of keys in the map +func (m *Files) Keys() []fs.Path { + var keys []fs.Path + m.Range(func(key fs.Path, value fs.File) bool { + keys = append(keys, key) + return true + }) + sort.Slice(keys, func(a, b int) bool { + return keys[a].String() <= keys[b].String() + }) + return keys +} + +func (m *Files) String() string { + return fmt.Sprintf("%v", m.Keys()) +} diff --git a/internal/maps/infos.go b/internal/maps/infos.go new file mode 100644 index 0000000..a0d4871 --- /dev/null +++ b/internal/maps/infos.go @@ -0,0 +1,126 @@ +// Code generated by github.com/markbates/pkger/mapgen. DO NOT EDIT. + +package maps + +import ( + "encoding/json" + "fmt" + "sort" + "sync" + + "github.com/markbates/pkger/here" +) + +// Infos wraps sync.Map and uses the following types: +// key: string +// value: here.Info +type Infos struct { + data *sync.Map + once *sync.Once +} + +func (m *Infos) Data() *sync.Map { + if m.once == nil { + m.once = &sync.Once{} + } + m.once.Do(func() { + if m.data == nil { + m.data = &sync.Map{} + } + }) + return m.data +} + +func (m *Infos) MarshalJSON() ([]byte, error) { + mm := map[string]interface{}{} + m.data.Range(func(key, value interface{}) bool { + mm[fmt.Sprintf("%s", key)] = value + return true + }) + return json.Marshal(mm) +} + +func (m *Infos) UnmarshalJSON(b []byte) error { + mm := map[string]here.Info{} + + if err := json.Unmarshal(b, &mm); err != nil { + return err + } + for k, v := range mm { + m.Store(k, v) + } + return nil +} + +// Delete the key from the map +func (m *Infos) Delete(key string) { + m.Data().Delete(key) +} + +// Load the key from the map. +// Returns here.Info or bool. +// A false return indicates either the key was not found +// or the value is not of type here.Info +func (m *Infos) Load(key string) (here.Info, bool) { + m.Data() + i, ok := m.data.Load(key) + if !ok { + return here.Info{}, false + } + s, ok := i.(here.Info) + return s, ok +} + +// LoadOrStore will return an existing key or +// store the value if not already in the map +func (m *Infos) LoadOrStore(key string, value here.Info) (here.Info, bool) { + i, _ := m.Data().LoadOrStore(key, value) + s, ok := i.(here.Info) + return s, ok +} + +// LoadOr will return an existing key or +// run the function and store the results +func (m *Infos) LoadOr(key string, fn func(*Infos) (here.Info, bool)) (here.Info, bool) { + i, ok := m.Load(key) + if ok { + return i, ok + } + i, ok = fn(m) + if ok { + m.Store(key, i) + return i, ok + } + return i, false +} + +// Range over the here.Info values in the map +func (m *Infos) Range(f func(key string, value here.Info) bool) { + m.Data().Range(func(k, v interface{}) bool { + key, ok := k.(string) + if !ok { + return false + } + value, ok := v.(here.Info) + if !ok { + return false + } + return f(key, value) + }) +} + +// Store a here.Info in the map +func (m *Infos) Store(key string, value here.Info) { + m.Data().Store(key, value) +} + +// Keys returns a list of keys in the map +func (m *Infos) Keys() []string { + var keys []string + m.Range(func(key string, value here.Info) bool { + keys = append(keys, key) + return true + }) + sort.Strings(keys) + return keys +} diff --git a/internal/maps/paths.go b/internal/maps/paths.go new file mode 100644 index 0000000..356bec9 --- /dev/null +++ b/internal/maps/paths.go @@ -0,0 +1,182 @@ +// Code generated by github.com/gobuffalo/mapgen. DO NOT EDIT. + +package maps + +import ( + "encoding/json" + "fmt" + "regexp" + "sort" + "strings" + "sync" + + "github.com/markbates/pkger/fs" + "github.com/markbates/pkger/here" +) + +// Paths wraps sync.Map and uses the following types: +// key: string +// value: Path +type Paths struct { + Current here.Info + data *sync.Map + once *sync.Once +} + +func (m *Paths) Data() *sync.Map { + if m.once == nil { + m.once = &sync.Once{} + } + m.once.Do(func() { + if m.data == nil { + m.data = &sync.Map{} + } + }) + return m.data +} + +func (m *Paths) MarshalJSON() ([]byte, error) { + mm := map[string]interface{}{} + m.Data().Range(func(key, value interface{}) bool { + mm[fmt.Sprintf("%s", key)] = value + return true + }) + return json.Marshal(mm) +} + +func (m *Paths) UnmarshalJSON(b []byte) error { + mm := map[string]fs.Path{} + + if err := json.Unmarshal(b, &mm); err != nil { + return err + } + for k, v := range mm { + m.Store(k, v) + } + return nil +} + +// Delete the key from the map +func (m *Paths) Delete(key string) { + m.Data().Delete(key) +} + +// Load the key from the map. +// Returns Path or bool. +// A false return indicates either the key was not found +// or the value is not of type Path +func (m *Paths) Load(key string) (fs.Path, bool) { + i, ok := m.Data().Load(key) + if !ok { + return fs.Path{}, false + } + s, ok := i.(fs.Path) + return s, ok +} + +// LoadOrStore will return an existing key or +// store the value if not already in the map +func (m *Paths) LoadOrStore(key string, value fs.Path) (fs.Path, bool) { + i, _ := m.Data().LoadOrStore(key, value) + s, ok := i.(fs.Path) + return s, ok +} + +// LoadOr will return an existing key or +// run the function and store the results +func (m *Paths) LoadOr(key string, fn func(*Paths) (fs.Path, bool)) (fs.Path, bool) { + i, ok := m.Load(key) + if ok { + return i, ok + } + i, ok = fn(m) + if ok { + m.Store(key, i) + return i, ok + } + return i, false +} + +// Range over the Path values in the map +func (m *Paths) Range(f func(key string, value fs.Path) bool) { + m.Data().Range(func(k, v interface{}) bool { + key, ok := k.(string) + if !ok { + return false + } + value, ok := v.(fs.Path) + if !ok { + return false + } + return f(key, value) + }) +} + +// Store a Path in the map +func (m *Paths) Store(key string, value fs.Path) { + m.Data().Store(key, value) +} + +// Keys returns a list of keys in the map +func (m *Paths) Keys() []string { + var keys []string + m.Range(func(key string, value fs.Path) bool { + keys = append(keys, key) + return true + }) + sort.Strings(keys) + return keys +} + +func (m *Paths) Parse(p string) (fs.Path, error) { + p = strings.Replace(p, "\\", "/", -1) + pt, ok := m.Load(p) + if ok { + return pt, nil + } + if len(p) == 0 { + return m.build(p, "", "") + } + + res := pathrx.FindAllStringSubmatch(p, -1) + if len(res) == 0 { + return pt, fmt.Errorf("could not parse %q", p) + } + + matches := res[0] + + if len(matches) != 4 { + return pt, fmt.Errorf("could not parse %q", p) + } + + return m.build(p, matches[1], matches[3]) +} + +var pathrx = regexp.MustCompile("([^:]+)(:(/.+))?") + +func (m *Paths) build(p, pkg, name string) (fs.Path, error) { + pt := fs.Path{ + Pkg: pkg, + Name: name, + } + + if strings.HasPrefix(pt.Pkg, "/") || len(pt.Pkg) == 0 { + pt.Name = pt.Pkg + pt.Pkg = m.Current.ImportPath + } + + if len(pt.Name) == 0 { + pt.Name = "/" + } + + if pt.Pkg == pt.Name { + pt.Pkg = m.Current.ImportPath + pt.Name = "/" + } + + if !strings.HasPrefix(pt.Name, "/") { + pt.Name = "/" + pt.Name + } + m.Store(p, pt) + return pt, nil +}