diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..01e48b4 --- /dev/null +++ b/doc.go @@ -0,0 +1,21 @@ +/* +This package is WIP. Please use/test/try and report issues, but be careful with production. OK? + +Pkger is powered by the dark magic of Go Modules, so they're like, totally required. + +With Go Modules pkger can resolve packages with accuracy. No more guessing and trying to +figure out build paths, GOPATHS, etc... for this tired old lad. + +With the module's path correctly resolved, it can serve as the "root" directory for that +module, and all files in that module's directory are available. + + Paths: + * Paths should use UNIX style paths: + /cmd/pkger/main.go + * If unspecified the path's package is assumed to be the current module. + * Packages can specified in at the beginning of a path with a `:` seperator. + github.com/markbates/pkger:/cmd/pkger/main.go + + "github.com/gobuffalo/buffalo:/go.mod" => $GOPATH/pkg/mod/github.com/gobuffalo/buffalo@v0.14.7/go.mod +*/ +package pkger diff --git a/file.go b/file.go index 382dd54..8f6b7d1 100644 --- a/file.go +++ b/file.go @@ -21,9 +21,74 @@ type File struct { path Path data []byte index *index + writer io.ReadWriter Source io.ReadCloser } +func (f *File) Close() error { + defer func() { + f.Source = nil + f.writer = nil + }() + if f.Source != nil { + if c, ok := f.Source.(io.Closer); ok { + if err := c.Close(); err != nil { + return err + } + } + } + + if f.writer == nil { + return nil + } + + b, err := ioutil.ReadAll(f.writer) + if err != nil { + return err + } + f.data = b + + fi := f.info + fi.size = int64(len(f.data)) + fi.modTime = time.Now() + f.info = fi + return nil +} + +func (f *File) Read(p []byte) (int, error) { + if len(f.data) > 0 && len(f.data) <= len(p) { + return copy(p, f.data), io.EOF + } + + if len(f.data) > 0 { + f.Source = ioutil.NopCloser(bytes.NewReader(f.data)) + } + + if f.Source != nil { + return f.Source.Read(p) + } + + of, err := f.her.Open(f.Path()) + if err != nil { + return 0, err + } + f.Source = of + return f.Source.Read(p) +} + +func (f *File) Write(b []byte) (int, error) { + if f.writer == nil { + f.writer = &bytes.Buffer{} + } + i, err := f.writer.Write(b) + fmt.Println(f.Name(), i, err) + return i, err +} + +func (f File) HereInfo() here.Info { + return f.her +} + func (f File) MarshalJSON() ([]byte, error) { m := map[string]interface{}{} m["info"] = f.info @@ -76,9 +141,7 @@ func (f *File) UnmarshalJSON(b []byte) error { if !ok { return fmt.Errorf("missing index") } - f.index = &index{ - Files: map[Path]*File{}, - } + f.index = newIndex() if err := json.Unmarshal(ind, f.index); err != nil { return err } @@ -87,9 +150,7 @@ func (f *File) UnmarshalJSON(b []byte) error { func (f *File) Open(name string) (http.File, error) { if f.index == nil { - f.index = &index{ - Files: map[Path]*File{}, - } + f.index = newIndex() } pt, err := Parse(name) if err != nil { @@ -148,16 +209,6 @@ func (f File) Stat() (os.FileInfo, error) { 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() } @@ -174,19 +225,6 @@ func (f File) String() string { return string(b) } -func (f *File) Read(p []byte) (int, error) { - if f.Source != nil { - return f.Source.Read(p) - } - - of, err := f.her.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. diff --git a/file_test.go b/file_test.go index 78a25ee..08ee4a9 100644 --- a/file_test.go +++ b/file_test.go @@ -1,7 +1,9 @@ package pkger import ( + "io" "io/ioutil" + "strings" "testing" "github.com/stretchr/testify/require" @@ -31,3 +33,48 @@ func Test_File_Open_Dir(t *testing.T) { r.NoError(f.Close()) } + +func Test_File_Read_Memory(t *testing.T) { + r := require.New(t) + + f, err := Open("/file_test.go") + r.NoError(err) + f.data = []byte("hi!") + + r.Equal("file_test.go", f.Name()) + + b, err := ioutil.ReadAll(f) + r.NoError(err) + r.Equal(string(b), "hi!") + r.NoError(f.Close()) +} + +func Test_File_Write(t *testing.T) { + r := require.New(t) + + i := newIndex() + + f, err := i.Create(Path{ + Name: "/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()) +} diff --git a/http_test.go b/http_test.go index 4a2ac6e..d5c2e04 100644 --- a/http_test.go +++ b/http_test.go @@ -48,3 +48,25 @@ func Test_HTTP_Dir(t *testing.T) { r.NoError(f.Close()) } + +func Test_HTTP_File_Memory(t *testing.T) { + r := require.New(t) + + i := newIndex() + f, err := createFile(i, "/cmd/pkger/main.go") + + r.NoError(err) + + ts := httptest.NewServer(http.FileServer(f)) + defer ts.Close() + + res, err := http.Get(ts.URL + "/cmd/pkger/main.go") + r.NoError(err) + r.Equal(200, res.StatusCode) + + b, err := ioutil.ReadAll(res.Body) + r.NoError(err) + r.Contains(string(b), "I wanna bite the hand that feeds me") + + r.NoError(f.Close()) +} diff --git a/index.go b/index.go index a162978..eac5c0e 100644 --- a/index.go +++ b/index.go @@ -6,6 +6,7 @@ import ( "os" "path/filepath" "strings" + "time" "github.com/gobuffalo/here" "github.com/markbates/pkger/pkgs" @@ -16,6 +17,26 @@ type index struct { Files map[Path]*File } +func (i index) Create(pt Path) (*File, error) { + her, err := pkgs.Pkg(pt.Pkg) + if err != nil { + return nil, err + } + f := &File{ + path: pt, + index: newIndex(), + her: her, + info: &FileInfo{ + name: pt.Name, + mode: 0666, + modTime: time.Now(), + }, + } + + i.Files[pt] = f + return f, nil +} + func (i index) MarshalJSON() ([]byte, error) { m := map[string]interface{}{ "pkg": i.Pkg, @@ -90,13 +111,11 @@ func (i index) Open(pt Path) (*File, error) { return i.openDisk(pt) } return &File{ - info: f.info, - path: f.path, - data: f.data, - her: f.her, - index: &index{ - Files: map[Path]*File{}, - }, + info: f.info, + path: f.path, + data: f.data, + her: f.her, + index: newIndex(), }, nil } @@ -128,6 +147,35 @@ func (i index) openDisk(pt Path) (*File, error) { return f, nil } -var rootIndex = &index{ - Files: map[Path]*File{}, +func (i index) 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, "/") + if len(pt.Pkg) == 0 { + pt.Pkg = i.Pkg + } + return pt, nil } + +func newIndex() *index { + return &index{ + Files: map[Path]*File{}, + } +} + +var rootIndex = newIndex() diff --git a/index_test.go b/index_test.go new file mode 100644 index 0000000..945c14b --- /dev/null +++ b/index_test.go @@ -0,0 +1,63 @@ +package pkger + +import ( + "io" + "os" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func Test_index_Create(t *testing.T) { + r := require.New(t) + + i := newIndex() + + f, err := i.Create(Path{ + Name: "/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()) + + her := f.her + r.NotZero(her) + r.Equal("github.com/markbates/pkger", her.ImportPath) +} + +func Test_index_Create_Write(t *testing.T) { + r := require.New(t) + + i := newIndex() + + f, err := i.Create(Path{ + Name: "/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()) +} diff --git a/internal/examples/extfile/main.go b/internal/examples/extfile/main.go index 72482ce..b76f955 100644 --- a/internal/examples/extfile/main.go +++ b/internal/examples/extfile/main.go @@ -10,7 +10,7 @@ import ( ) func main() { - f, err := pkger.Open("github.com/gobuffalo/buffalo:/server.go") + f, err := pkger.Open("github.com/gobuffalo/buffalo:/go.mod") if err != nil { log.Fatal("1", err) } diff --git a/path.go b/path.go index af09b40..4411a4d 100644 --- a/path.go +++ b/path.go @@ -2,7 +2,6 @@ package pkger import ( "fmt" - "strings" ) type Path struct { @@ -21,23 +20,5 @@ func (p Path) String() string { } 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 + return rootIndex.Parse(p) } diff --git a/pkger.go b/pkger.go index aabc11b..607d799 100644 --- a/pkger.go +++ b/pkger.go @@ -1,31 +1,6 @@ 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() -} - +// Open opens the named file for reading. func Open(p string) (*File, error) { pt, err := Parse(p) if err != nil { @@ -33,3 +8,12 @@ func Open(p string) (*File, error) { } return rootIndex.Open(pt) } + +// Create creates the named file with mode 0666 (before umask), truncating it if it already exists. If successful, methods on the returned File can be used for I/O; the associated file descriptor has mode O_RDWR. If there is an error, it will be of type *PathError. +func Create(p string) (*File, error) { + pt, err := Parse(p) + if err != nil { + return nil, err + } + return rootIndex.Create(pt) +} diff --git a/pkger_test.go b/pkger_test.go new file mode 100644 index 0000000..030c6b4 --- /dev/null +++ b/pkger_test.go @@ -0,0 +1,68 @@ +package pkger + +import ( + "io" + "strings" +) + +func createFile(i *index, p string, body ...string) (*File, error) { + pt, err := i.Parse(p) + if err != nil { + return nil, err + } + + if len(body) == 0 { + body = append(body, radio) + } + + f, err := i.Create(pt) + if err != nil { + return nil, err + } + + _, err = io.Copy(f, strings.NewReader(strings.Join(body, "\n\n"))) + if err != nil { + return nil, err + } + + if err := f.Close(); err != nil { + return nil, err + } + return f, nil +} + +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`