Browse Source

Cleanup the FS implementationfor vessel

spsobole 3 years ago
parent
commit
781eadc3f6
11 changed files with 342 additions and 140 deletions
  1. 1 0
      .gitignore
  2. 53 0
      build.yaml
  3. 4 2
      bundle.go
  4. 4 0
      cmd/vesselcmd/main.go
  5. 1 1
      example/site/templates/list.html
  6. 2 0
      go.mod
  7. 10 0
      go.sum
  8. 73 0
      http_dir.go
  9. 59 0
      http_file.go
  10. 74 137
      http_fs.go
  11. 61 0
      vessel_test.go

+ 1 - 0
.gitignore

@@ -26,4 +26,5 @@ _testmain.go
 
 ./vesselcmd
 .idea/
+build/
 .DS_Store

+ 53 - 0
build.yaml

@@ -0,0 +1,53 @@
+#
+# Build config for ThirdMartini go-builder
+#
+gobuild.version.required: 1.0
+gobuild.work.dir: build
+
+properties:
+  version: git rev-parse --short=12 HEAD
+
+jobs:
+  #
+  # Format source code
+  format:
+    - name: Format source code
+      type: golang
+      source: git.thirdmartini.com/thirdmartini/vessel/...
+      options:
+      flags:
+
+  #
+  # Run some tests
+  test:
+    - name: Running unit tests
+      type: golang
+      source: git.thirdmartini.com/thirdmartini/vessel/...
+      options:
+      flags:
+
+  #
+  # Build binary for different architectures
+  build:
+    - name: Build Binaries
+      type: golang
+      source: git.thirdmartini.com/thirdmartini/vessel/cmd/vesselcmd
+      targets:
+        - artifact: build/vesselcmd
+        - artifact: build/vesselcmd.arm7.linux
+          arch: arm
+          os: linux
+          model: 7
+        - artifact: build/vesselcmd.arm64.linux
+          arch: arm64
+          os: linux
+
+  deploy:
+    - name: Copy native binaries localy
+      type: copy
+      artifact: vesselcmd
+      targets:
+        - artifact: build/vesselcmd
+      options:
+        source: build/vesselcmd
+        destination: ~/bin/vesselcmd

+ 4 - 2
bundle.go

@@ -64,9 +64,12 @@ func (b *Bundle) ReadAt(p []byte, off int64) (n int, err error) {
 
 // FileSystem returns a handle to an bundle as a FileSystem that implements http.Filesystem
 func (b *Bundle) FileSystem() *FileSystem {
-	return &FileSystem{
+	f := &FileSystem{
 		b: b,
 	}
+
+	f.initCaches()
+	return f
 }
 
 // tryLoadEmbeddedBundle loads the bundle from an embedded file
@@ -179,7 +182,6 @@ func concatenate(dst string, src string) error {
 
 	dstOut.Seek(0, io.SeekEnd)
 
-	log.Log("Glueing...\n")
 	_, err = io.Copy(dstOut, srcIn)
 	if err != nil {
 		return fmt.Errorf("error glueing: %w", err)

+ 4 - 0
cmd/vesselcmd/main.go

@@ -30,6 +30,10 @@ func main() {
 	global := flag.NewFlagSet("*", flag.ContinueOnError)
 	debugFlag := global.Bool("debug", false, "enable debug info")
 
+	if len(os.Args) <= 1 {
+		help()
+	}
+
 	global.Parse(os.Args)
 	if *debugFlag {
 		vessel.SetLogger(&vessel.DebugLogger{})

+ 1 - 1
example/site/templates/list.html

@@ -65,7 +65,7 @@
         <tbody>
         {{range . }}
             <tr>
-                <td><a href="{{ . }}">{{ . }}</td>
+                <td><a href="{{ . }}">{{ . }}</a></td>
             </tr>
         {{end}}
         </tbody>

+ 2 - 0
go.mod

@@ -1,3 +1,5 @@
 module git.thirdmartini.com/thirdmartini/vessel
 
 go 1.15
+
+require github.com/stretchr/testify v1.7.0

+ 10 - 0
go.sum

@@ -0,0 +1,10 @@
+github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

+ 73 - 0
http_dir.go

@@ -0,0 +1,73 @@
+package vessel
+
+import (
+	"io"
+	"net/http"
+	"os"
+	"time"
+)
+
+// File implements an http.File
+type Dir struct {
+	name string
+	mode os.FileMode
+	fs   *FileSystem
+}
+
+// verify we implement the http.File interface
+var _ http.File = (*Dir)(nil)
+var _ os.FileInfo = (*Dir)(nil)
+
+// Readdir implements http.File
+func (f *Dir) Readdir(count int) ([]os.FileInfo, error) {
+	files, err := f.fs.Dir(f.name)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(files) < count {
+		count = len(files)
+	}
+	return files[:count], nil
+}
+
+// Stat implements http.File
+func (f *Dir) Stat() (os.FileInfo, error) {
+	return f, nil
+}
+
+// Seek implements io.Seeker
+func (f *Dir) Seek(offset int64, whence int) (int64, error) {
+	return 0, os.ErrInvalid
+}
+
+// Read implements io.Reader
+func (f *Dir) Read(p []byte) (n int, err error) {
+	return 0, io.EOF
+}
+
+// Close implements io.Closer
+func (f *Dir) Close() error {
+	return nil
+}
+
+// implement file info interface as well
+func (f *Dir) Name() string {
+	return f.name
+}
+
+func (f *Dir) Size() int64 {
+	return 0
+}
+func (f *Dir) Mode() os.FileMode {
+	return f.mode
+}
+func (f *Dir) ModTime() time.Time {
+	return time.Now()
+}
+func (f *Dir) IsDir() bool {
+	return true
+}
+func (f *Dir) Sys() interface{} {
+	return nil
+}

+ 59 - 0
http_file.go

@@ -0,0 +1,59 @@
+package vessel
+
+import (
+	"archive/zip"
+	"io"
+	"net/http"
+	"os"
+)
+
+// File implements an http.File
+type File struct {
+	r      io.ReadCloser
+	zip    *zip.File
+	offset int64
+}
+
+// verify we implement the http.File interface
+var _ http.File = (*File)(nil)
+
+func newFile(z *zip.File) (*File, error) {
+	r, err := z.Open()
+	if err != nil {
+		return nil, err
+	}
+	return &File{
+		r:   r,
+		zip: z,
+	}, nil
+}
+
+// Readdir implements http.File
+func (f *File) Readdir(count int) ([]os.FileInfo, error) {
+	log.Log("read dir: %s\n", f.zip.Name)
+	return nil, os.ErrPermission
+}
+
+// Stat implements http.File
+func (f *File) Stat() (os.FileInfo, error) {
+	return f.zip.FileInfo(), nil
+}
+
+// Seek implements io.Seeker
+func (f *File) Seek(offset int64, whence int) (int64, error) {
+	return f.offset, os.ErrInvalid
+}
+
+// Read implements io.Reader
+func (f *File) Read(p []byte) (n int, err error) {
+	c, err := f.r.Read(p)
+	if err != nil {
+		f.offset += int64(c)
+	}
+	return c, err
+}
+
+// Close implements io.Closer
+func (f *File) Close() error {
+	return f.r.Close()
+}

+ 74 - 137
http_fs.go

@@ -2,124 +2,18 @@ package vessel
 
 import (
 	"archive/zip"
-	"io"
+	"fmt"
 	"net/http"
 	"os"
+	"path"
 	"path/filepath"
-	"time"
+	"strings"
 )
 
-// File implements an http.File
-type File struct {
-	r      io.ReadCloser
-	zip    *zip.File
-	offset int64
-}
-
-// verify we implement the http.File interface
-var _ http.File = (*File)(nil)
-
-func newFile(z *zip.File) (*File, error) {
-	r, err := z.Open()
-	if err != nil {
-		return nil, err
-	}
-	return &File{
-		r:   r,
-		zip: z,
-	}, nil
-}
-
-// Readdir implements http.File
-func (f *File) Readdir(count int) ([]os.FileInfo, error) {
-	log.Log("read dir: %s\n", f.zip.Name)
-	return nil, os.ErrPermission
-}
-
-// Stat implements http.File
-func (f *File) Stat() (os.FileInfo, error) {
-	return f.zip.FileInfo(), nil
-}
-
-// Seek implements io.Seeker
-func (f *File) Seek(offset int64, whence int) (int64, error) {
-	return f.offset, os.ErrInvalid
-}
-
-// Read implements io.Reader
-func (f *File) Read(p []byte) (n int, err error) {
-	c, err := f.r.Read(p)
-	if err != nil {
-		f.offset += int64(c)
-	}
-	return c, err
-}
-
-// Close implements io.Closer
-func (f *File) Close() error {
-	return f.r.Close()
-}
-
-// File implements an http.File
-type Dir struct {
-	name  string
-	mode  os.FileMode
-	files []os.FileInfo
-}
-
-// verify we implement the http.File interface
-var _ http.File = (*Dir)(nil)
-var _ os.FileInfo = (*Dir)(nil)
-
-// Readdir implements http.File
-func (f *Dir) Readdir(count int) ([]os.FileInfo, error) {
-	return f.files[:count], nil
-}
-
-// Stat implements http.File
-func (f *Dir) Stat() (os.FileInfo, error) {
-	return f, nil
-}
-
-// Seek implements io.Seeker
-func (f *Dir) Seek(offset int64, whence int) (int64, error) {
-	return 0, os.ErrInvalid
-}
-
-// Read implements io.Reader
-func (f *Dir) Read(p []byte) (n int, err error) {
-	return 0, io.EOF
-}
-
-// Close implements io.Closer
-func (f *Dir) Close() error {
-	return nil
-}
-
-// implement file info interface as well
-func (f *Dir) Name() string {
-	return f.name
-}
-
-func (f *Dir) Size() int64 {
-	return 0
-}
-func (f *Dir) Mode() os.FileMode {
-	return f.mode
-}
-func (f *Dir) ModTime() time.Time {
-	return time.Now()
-}
-func (f *Dir) IsDir() bool {
-	return true
-}
-func (f *Dir) Sys() interface{} {
-	return nil
-}
-
 // FileSystem implements an http filesystem access to the bundle
 type FileSystem struct {
-	b *Bundle
+	b     *Bundle
+	files map[string]os.FileInfo
 }
 
 // verify we implement the http.FileSystem interface
@@ -143,18 +37,18 @@ func (f *FileSystem) get(name string) (*zip.File, error) {
 
 // Open opens a file in the filesystem
 func (f *FileSystem) Open(name string) (http.File, error) {
-	log.Log("vessel.FileSystem Open: %s\n", name)
-	if filepath.Dir(name) == name {
-		// directory fetch
-		files, err := f.Dir(name)
-		if err != nil {
-			return nil, err
-		}
+	log.Log("vessel.FileSystem Open: %s|%s\n", name, filepath.Dir(name))
 
+	v, ok := f.files[name]
+	if !ok {
+		return nil, os.ErrNotExist
+	}
+
+	if v.IsDir() {
 		return &Dir{
-			name:  name,
-			mode:  os.ModeDir,
-			files: files,
+			name: name,
+			mode: os.ModeDir,
+			fs:   f,
 		}, nil
 	}
 
@@ -172,35 +66,78 @@ func (f *FileSystem) Open(name string) (http.File, error) {
 
 // Stat returns file info for a file in the bundle filesystem
 func (f *FileSystem) Stat(name string) (os.FileInfo, error) {
-	z, err := f.get(name)
-	if err != nil {
-		return nil, err
+	v, ok := f.files[name]
+	if !ok {
+		return nil, os.ErrNotExist
 	}
-	return z.FileInfo(), nil
+
+	return v, nil
+}
+
+func (f *FileSystem) isDir(path string) bool {
+	if path == string(os.PathSeparator) {
+		return true
+	}
+
+	v, ok := f.files[path]
+	if !ok {
+		return false
+	}
+	return v.IsDir()
 }
 
 // Dir return a list of files contained in a subdirectory of our bundle
 func (f *FileSystem) Dir(path string) ([]os.FileInfo, error) {
-	zr, err := zip.NewReader(f.b, f.b.Size())
-	if err != nil {
+	if f.isDir(path) == false {
 		return nil, os.ErrNotExist
 	}
 
-	found := false
 	ls := make([]os.FileInfo, 0)
-	for idx := range zr.File {
-		z := zr.File[idx]
+	for k := range f.files {
+		if k == path {
+			continue
+		}
 
-		d := filepath.Dir(z.Name)
-		if d == path && !z.FileInfo().IsDir() {
-			found = true
-			ls = append(ls, z.FileInfo())
+		d := filepath.Dir(k)
+		if d == path {
+			fmt.Printf("+%s\n", k)
+			ls = append(ls, f.files[k])
 		}
 	}
+	return ls, nil
+}
 
-	if !found {
-		return nil, os.ErrNotExist
+func (f *FileSystem) initCaches() error {
+	zr, err := zip.NewReader(f.b, f.b.Size())
+	if err != nil {
+		return os.ErrNotExist
 	}
 
-	return ls, nil
+	files := make(map[string]os.FileInfo)
+	for idx := range zr.File {
+		z := zr.File[idx]
+		files[z.Name] = z.FileInfo()
+	}
+
+	// zip files don't contain directories
+	for k, v := range files {
+		if v.IsDir() {
+			continue
+		}
+
+		dir := filepath.Dir(k)
+		chunks := strings.Split(dir, string(os.PathSeparator))
+		for i := 2; i <= len(chunks); i++ {
+			dir := fmt.Sprintf("/%s", path.Join(chunks[:i]...))
+			if _, ok := files[dir]; !ok {
+				files[dir] = &Dir{
+					name: dir,
+					mode: os.ModeDir,
+					fs:   f,
+				}
+			}
+		}
+	}
+	f.files = files
+	return nil
 }

+ 61 - 0
vessel_test.go

@@ -0,0 +1,61 @@
+package vessel
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestVessel(t *testing.T) {
+	ename := "./test.bundle"
+
+	SetLogger(DebugLogger{})
+
+	f, err := os.Create(ename)
+	assert.Nil(t, err)
+	f.Close()
+	defer os.Remove(ename)
+
+	b, err := Build(ename, "example", true, func(path string) bool {
+		base := filepath.Base(path)
+		if base == ".DS_Store" {
+			return false
+		}
+		if strings.HasSuffix(base, ".zip") {
+			return false
+		}
+		return true
+	})
+	assert.Nil(t, err)
+	assert.NotNil(t, b)
+
+	fs := b.FileSystem()
+	assert.NotNil(t, fs)
+
+	files, err := fs.Dir("/")
+	assert.Nil(t, err)
+	assert.Equal(t, 3, len(files))
+	fmt.Println("E----", len(files))
+
+	fmt.Println("S----")
+	files, err = fs.Dir("/site")
+	assert.Nil(t, err)
+	assert.Equal(t, 4, len(files))
+	fmt.Println("E----", len(files))
+
+	stat, err := fs.Stat("/site")
+	assert.Nil(t, err)
+	assert.Equal(t, true, stat.IsDir())
+
+	stat, err = fs.Stat("/site/static")
+	assert.Nil(t, err)
+	assert.Equal(t, true, stat.IsDir())
+
+	stat, err = fs.Stat("/site/templates/list.html")
+	assert.Nil(t, err)
+	assert.Equal(t, false, stat.IsDir())
+}