Browse Source

Add a fully flushed out server

spsobole 3 years ago
parent
commit
86da497ddd
32 changed files with 3721 additions and 42 deletions
  1. 2 1
      .gitignore
  2. 16 9
      bundle.go
  3. 22 2
      cmd/vesselcmd/main.go
  4. 8 0
      example/Makefile
  5. 47 5
      example/server.go
  6. BIN
      example/site/favicon.ico
  7. 66 5
      example/site/index.html
  8. 4 0
      example/site/static/semantic/jquery-3.1.1.min.js
  9. 372 0
      example/site/static/semantic/semantic.min.css
  10. 11 0
      example/site/static/semantic/semantic.min.js
  11. BIN
      example/site/static/semantic/themes/default/assets/fonts/brand-icons.eot
  12. 1008 0
      example/site/static/semantic/themes/default/assets/fonts/brand-icons.svg
  13. BIN
      example/site/static/semantic/themes/default/assets/fonts/brand-icons.ttf
  14. BIN
      example/site/static/semantic/themes/default/assets/fonts/brand-icons.woff
  15. BIN
      example/site/static/semantic/themes/default/assets/fonts/brand-icons.woff2
  16. BIN
      example/site/static/semantic/themes/default/assets/fonts/icons.eot
  17. BIN
      example/site/static/semantic/themes/default/assets/fonts/icons.otf
  18. 1518 0
      example/site/static/semantic/themes/default/assets/fonts/icons.svg
  19. BIN
      example/site/static/semantic/themes/default/assets/fonts/icons.ttf
  20. BIN
      example/site/static/semantic/themes/default/assets/fonts/icons.woff
  21. BIN
      example/site/static/semantic/themes/default/assets/fonts/icons.woff2
  22. BIN
      example/site/static/semantic/themes/default/assets/fonts/outline-icons.eot
  23. 366 0
      example/site/static/semantic/themes/default/assets/fonts/outline-icons.svg
  24. BIN
      example/site/static/semantic/themes/default/assets/fonts/outline-icons.ttf
  25. BIN
      example/site/static/semantic/themes/default/assets/fonts/outline-icons.woff
  26. BIN
      example/site/static/semantic/themes/default/assets/fonts/outline-icons.woff2
  27. BIN
      example/site/static/semantic/themes/default/assets/images/flags.png
  28. 75 0
      example/site/templates/list.html
  29. 125 13
      http_fs.go
  30. 29 0
      log.go
  31. 28 0
      overlay.go
  32. 24 7
      vessel.go

+ 2 - 1
.gitignore

@@ -24,5 +24,6 @@ _testmain.go
 *.test
 *.prof
 
-vesselcmd
+./vesselcmd
 .idea/
+.DS_Store

+ 16 - 9
bundle.go

@@ -158,7 +158,7 @@ func (b *Bundle) Decompress(path string) error {
 }
 
 func concatenate(dst string, src string) error {
-	fmt.Printf("Attaching: %s -> %s\n", src, dst)
+	log.Log("Attaching: %s -> %s\n", src, dst)
 
 	srcIn, err := os.Open(src)
 	if err != nil {
@@ -179,7 +179,7 @@ func concatenate(dst string, src string) error {
 
 	dstOut.Seek(0, io.SeekEnd)
 
-	fmt.Printf("Glueing...\n")
+	log.Log("Glueing...\n")
 	_, err = io.Copy(dstOut, srcIn)
 	if err != nil {
 		return fmt.Errorf("error glueing: %w", err)
@@ -203,14 +203,25 @@ func concatenate(dst string, src string) error {
 	return err
 }
 
-func packInto(dst *os.File, src string) error {
+func packInto(dst *os.File, src string, filter func(file string) bool) error {
 	// Create a new zip archive.
 	w := zip.NewWriter(dst)
 
+	src = filepath.Clean(src)
+
 	var walker func(path string, info os.FileInfo, err error) error
 
 	walker = func(path string, info os.FileInfo, err error) error {
-		fmt.Printf("Crawling: %#v\n", path)
+		if filter != nil {
+			if filter(path) == false {
+				return nil
+			}
+		}
+
+		// truncated path
+		spath := strings.TrimPrefix(path, src)
+
+		log.Log("Crawling: [%s] %s as %s  \n", src, path, spath)
 		if err != nil {
 			return err
 		}
@@ -232,7 +243,7 @@ func packInto(dst *os.File, src string) error {
 		// This snippet happens to work because I don't use
 		// absolute paths, but ensure your real-world code
 		// transforms path into a zip-root relative path.
-		f, err := w.Create(path)
+		f, err := w.Create(spath)
 		if err != nil {
 			return err
 		}
@@ -252,7 +263,3 @@ func packInto(dst *os.File, src string) error {
 	// Make sure to check the error on Close.
 	return w.Close()
 }
-
-func build() {
-
-}

+ 22 - 2
cmd/vesselcmd/main.go

@@ -4,6 +4,8 @@ import (
 	"flag"
 	"fmt"
 	"os"
+	"path/filepath"
+	"strings"
 
 	"git.thirdmartini.com/thirdmartini/vessel"
 )
@@ -23,6 +25,15 @@ func main() {
 	bundleFlag := parms.String("bundle", "", "bundle optional zip into this file")
 	srcFlag := parms.String("src", "", "source for bundle")
 	dstFlag := parms.String("dst", "", "destination for bundle")
+	overwriteFlag := parms.Bool("overwrite", false, "overwrite destination bundle if it exists")
+
+	global := flag.NewFlagSet("*", flag.ContinueOnError)
+	debugFlag := global.Bool("debug", false, "enable debug info")
+
+	global.Parse(os.Args)
+	if *debugFlag {
+		vessel.SetLogger(&vessel.DebugLogger{})
+	}
 
 	switch os.Args[1] {
 	default:
@@ -46,7 +57,7 @@ func main() {
 
 	case "bundle":
 		parms.Parse(os.Args[2:])
-		b, err := vessel.Create(*bundleFlag, *srcFlag)
+		b, err := vessel.Create(*bundleFlag, *srcFlag, *overwriteFlag)
 		if err != nil {
 			panic(err)
 		}
@@ -56,7 +67,16 @@ func main() {
 
 	case "build":
 		parms.Parse(os.Args[2:])
-		b, err := vessel.Build(*bundleFlag, *srcFlag)
+		b, err := vessel.Build(*bundleFlag, *srcFlag, *overwriteFlag, func(path string) bool {
+			base := filepath.Base(path)
+			if base == ".DS_Store" {
+				return false
+			}
+			if strings.HasSuffix(base, ".zip") {
+				return false
+			}
+			return true
+		})
 		if err != nil {
 			panic(err)
 		}

+ 8 - 0
example/Makefile

@@ -0,0 +1,8 @@
+#
+# Builds the server binary and then packages site/ into it
+#
+
+all:
+	go build git.thirdmartini.com/thirdmartini/vessel/cmd/vesselcmd
+	go build -o server git.thirdmartini.com/thirdmartini/vessel/example/
+	./vesselcmd build --overwrite --bundle=./server --src=site/

+ 47 - 5
example/server.go

@@ -3,15 +3,16 @@ package main
 import (
 	"flag"
 	"fmt"
-	"html"
+	"html/template"
+	"io/ioutil"
 	"net/http"
 
 	"git.thirdmartini.com/thirdmartini/vessel"
 )
 
 func main() {
-	//http.Handle("/site/", vessel.NewZipFileHandler("example/site/site.zip", "/site/"))
 	extractFlag := flag.String("extract-bundle", "", "--extract-bundle=<extract point>")
+	localContentFlag := flag.String("local-content", "", "serve content from local site/")
 	flag.Parse()
 
 	if *extractFlag != "" {
@@ -32,9 +33,50 @@ func main() {
 		panic(err)
 	}
 
-	http.HandleFunc("/index.html", func(w http.ResponseWriter, r *http.Request) {
-		fmt.Fprintf(w, "Hello, %q", html.EscapeString(r.URL.Path))
+	// We're using an overlay FS so that we can transparently prefer things
+	// localy over our built bundle in the source for testing.
+	// this lets us make uick changes to the site/ content without having to regenerate a bundle
+	o := vessel.NewOverlayFS()
+	if *localContentFlag != "" {
+		o.Append(http.FileSystem(http.Dir(*localContentFlag)))
+	}
+	o.Append(b.FileSystem())
+
+	http.Handle("/", http.FileServer(o))
+	http.HandleFunc("/list", func(w http.ResponseWriter, r *http.Request) {
+		f, err := o.Open("/templates/list.html")
+		if err != nil {
+			w.WriteHeader(http.StatusNotFound)
+			w.Write([]byte(err.Error()))
+			return
+		}
+
+		data, err := ioutil.ReadAll(f)
+		if err != nil {
+			w.WriteHeader(http.StatusNotFound)
+			w.Write([]byte(err.Error()))
+			return
+		}
+
+		t, err := template.New("list").Parse(string(data))
+		if err != nil {
+			w.WriteHeader(http.StatusInternalServerError)
+			w.Write([]byte(err.Error()))
+			return
+		}
+		w.WriteHeader(http.StatusOK)
+		files := b.List()
+		t.Execute(w, files)
 	})
-	http.Handle("/", http.FileServer(b.FileSystem()))
+
+	http.HandleFunc("/listraw", func(w http.ResponseWriter, r *http.Request) {
+		w.WriteHeader(http.StatusOK)
+		files := b.List()
+
+		for _, n := range files {
+			fmt.Fprintf(w, "%s\n", n)
+		}
+	})
+
 	http.ListenAndServe(":8080", nil)
 }

BIN
example/site/favicon.ico


+ 66 - 5
example/site/index.html

@@ -1,10 +1,71 @@
-<!DOCTYPE html>
-<html lang="en">
+<html>
 <head>
-    <meta charset="UTF-8">
-    <title>Title</title>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
+    <link rel="shortcut icon" href="/favicon.ico" />
+
+    <link rel="stylesheet" type="text/css" href="/static/semantic/semantic.min.css">
+    <script src="/static/semantic/jquery-3.1.1.min.js"></script>
+    <script src="/static/semantic/semantic.min.js"></script>
+
+    <title>ThirdMartini Vessel Example</title>
+
+    <meta name="theme-color" content="#ff5343">
+    <script>
+        $(document)
+            .ready(function() {
+
+                // fix main menu to page on passing
+                $('.main.menu').visibility({
+                    type: 'fixed'
+                });
+                $('.overlay').visibility({
+                    type: 'fixed',
+                    offset: 80
+                });
+
+                // lazy load images
+                $('.image').visibility({
+                    type: 'image',
+                    transition: 'vertical flip in',
+                    duration: 500
+                });
+
+                // show dropdown on hover
+                $('.main.menu  .ui.dropdown').dropdown({
+                    on: 'hover'
+                });
+            })
+        ;
+    </script>
+
 </head>
 <body>
 
+<div class="ui main text container">
+    <h1 class="ui header">Vessel Example</h1>
+    <p>This example shows we can use an zip file embedded in our server executable to serve content</p>
+</div>
+
+<div class="ui borderless main menu">
+    <div class="ui text container">
+        <div class="header item">
+            <img class="logo" src="/favicon.ico">
+            &nbsp;&nbsp;Vessel Server Example
+
+        </div>
+    </div>
+</div>
+
+<div class="ui text container">
+    <div class="ui text container">
+        <h3 class="ui header">
+            <center>
+        <a href="/list">Show Contents of Bundle</a>
+            </center>
+        </h3>
+    </div>
+</div>
 </body>
-</html>
+</html>

File diff suppressed because it is too large
+ 4 - 0
example/site/static/semantic/jquery-3.1.1.min.js


File diff suppressed because it is too large
+ 372 - 0
example/site/static/semantic/semantic.min.css


File diff suppressed because it is too large
+ 11 - 0
example/site/static/semantic/semantic.min.js


BIN
example/site/static/semantic/themes/default/assets/fonts/brand-icons.eot


File diff suppressed because it is too large
+ 1008 - 0
example/site/static/semantic/themes/default/assets/fonts/brand-icons.svg


BIN
example/site/static/semantic/themes/default/assets/fonts/brand-icons.ttf


BIN
example/site/static/semantic/themes/default/assets/fonts/brand-icons.woff


BIN
example/site/static/semantic/themes/default/assets/fonts/brand-icons.woff2


BIN
example/site/static/semantic/themes/default/assets/fonts/icons.eot


BIN
example/site/static/semantic/themes/default/assets/fonts/icons.otf


File diff suppressed because it is too large
+ 1518 - 0
example/site/static/semantic/themes/default/assets/fonts/icons.svg


BIN
example/site/static/semantic/themes/default/assets/fonts/icons.ttf


BIN
example/site/static/semantic/themes/default/assets/fonts/icons.woff


BIN
example/site/static/semantic/themes/default/assets/fonts/icons.woff2


BIN
example/site/static/semantic/themes/default/assets/fonts/outline-icons.eot


File diff suppressed because it is too large
+ 366 - 0
example/site/static/semantic/themes/default/assets/fonts/outline-icons.svg


BIN
example/site/static/semantic/themes/default/assets/fonts/outline-icons.ttf


BIN
example/site/static/semantic/themes/default/assets/fonts/outline-icons.woff


BIN
example/site/static/semantic/themes/default/assets/fonts/outline-icons.woff2


BIN
example/site/static/semantic/themes/default/assets/images/flags.png


+ 75 - 0
example/site/templates/list.html

@@ -0,0 +1,75 @@
+<html>
+<head data-suburl="">
+    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+    <meta name="author" content="ThirdMartini" />
+    <meta name="description" content="Vessel" />
+
+    <link rel="shortcut icon" href="/favicon.ico" />
+
+    <link rel="stylesheet" type="text/css" href="/static/semantic/semantic.min.css">
+    <script src="/static/semantic/jquery-3.1.1.min.js"></script>
+    <script src="/static/semantic/semantic.min.js"></script>
+
+    <title>ThirdMartini Vessel Example</title>
+    <meta name="theme-color" content="#ff5343">
+    <script>
+        $(document)
+            .ready(function() {
+
+                // fix main menu to page on passing
+                $('.main.menu').visibility({
+                    type: 'fixed'
+                });
+                $('.overlay').visibility({
+                    type: 'fixed',
+                    offset: 80
+                });
+
+                // lazy load images
+                $('.image').visibility({
+                    type: 'image',
+                    transition: 'vertical flip in',
+                    duration: 500
+                });
+
+                // show dropdown on hover
+                $('.main.menu  .ui.dropdown').dropdown({
+                    on: 'hover'
+                });
+            })
+        ;
+    </script>
+</head>
+<body>
+
+<div class="ui main text container">
+    <h1 class="ui header">Vessel Example</h1>
+    <p>This example shows we can use an zip file embedded in our server executable to serve content</p>
+</div>
+
+<div class="ui borderless main menu">
+    <div class="ui text container">
+        <div class="header item">
+            <img class="logo" src="/favicon.ico">
+            &nbsp; Vessel Server Example
+        </div>
+    </div>
+</div>
+
+<div class="ui text container">
+    <table class="ui fixed table">
+        <thead>
+        </tr>
+        </thead>
+        <tbody>
+        {{range . }}
+            <tr>
+                <td><a href="{{ . }}">{{ . }}</td>
+            </tr>
+        {{end}}
+        </tbody>
+    </table>
+</div>
+</body>
+</html>

+ 125 - 13
http_fs.go

@@ -2,11 +2,11 @@ package vessel
 
 import (
 	"archive/zip"
-	"fmt"
 	"io"
 	"net/http"
 	"os"
-	"path"
+	"path/filepath"
+	"time"
 )
 
 // File implements an http.File
@@ -22,7 +22,6 @@ var _ http.File = (*File)(nil)
 func newFile(z *zip.File) (*File, error) {
 	r, err := z.Open()
 	if err != nil {
-		fmt.Printf("Open: %s -> %w\n", z.Name, err)
 		return nil, err
 	}
 	return &File{
@@ -33,7 +32,7 @@ func newFile(z *zip.File) (*File, error) {
 
 // Readdir implements http.File
 func (f *File) Readdir(count int) ([]os.FileInfo, error) {
-	fmt.Printf("read dir: %s\n", f.zip.Name)
+	log.Log("read dir: %s\n", f.zip.Name)
 	return nil, os.ErrPermission
 }
 
@@ -61,6 +60,63 @@ 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
@@ -69,24 +125,80 @@ type FileSystem struct {
 // verify we implement the http.FileSystem interface
 var _ http.FileSystem = (*FileSystem)(nil)
 
-// Open opens a file in the filesystem
-func (f *FileSystem) Open(name string) (http.File, error) {
+func (f *FileSystem) get(name string) (*zip.File, error) {
 	zr, err := zip.NewReader(f.b, f.b.Size())
 	if err != nil {
 		return nil, os.ErrNotExist
 	}
 
-	name = path.Join(zr.File[0].Name, name)
-	fmt.Printf("Open: %s\n", name)
-
 	for idx := range zr.File {
 		z := zr.File[idx]
 		if z.Name == name {
-			if !z.FileInfo().IsDir() {
-				return newFile(z)
-			}
-			return nil, os.ErrNotExist
+			return z, nil
+		}
+	}
+	return nil, os.ErrNotExist
+
+}
+
+// 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
 		}
+
+		return &Dir{
+			name:  name,
+			mode:  os.ModeDir,
+			files: files,
+		}, nil
 	}
+
+	z, err := f.get(name)
+	if err != nil {
+		return nil, err
+	}
+
+	if !z.FileInfo().IsDir() {
+		return newFile(z)
+	}
+
 	return nil, os.ErrNotExist
 }
+
+func (f *FileSystem) Stat(name string) (os.FileInfo, error) {
+	z, err := f.get(name)
+	if err != nil {
+		return nil, err
+	}
+	return z.FileInfo(), nil
+}
+
+func (f *FileSystem) Dir(path string) ([]os.FileInfo, error) {
+	zr, err := zip.NewReader(f.b, f.b.Size())
+	if err != nil {
+		return nil, os.ErrNotExist
+	}
+
+	found := false
+	ls := make([]os.FileInfo, 0)
+	for idx := range zr.File {
+		z := zr.File[idx]
+
+		d := filepath.Dir(z.Name)
+		if d == path && !z.FileInfo().IsDir() {
+			found = true
+			ls = append(ls, z.FileInfo())
+		}
+	}
+
+	if !found {
+		return nil, os.ErrNotExist
+	}
+
+	return ls, nil
+}

+ 29 - 0
log.go

@@ -0,0 +1,29 @@
+package vessel
+
+import (
+	golog "log"
+)
+
+var log Logger = &NilLogger{}
+
+type Logger interface {
+	Log(format string, a ...interface{})
+}
+
+type NilLogger struct {
+}
+
+func (l NilLogger) Log(format string, a ...interface{}) {}
+
+type DebugLogger struct {
+}
+
+func (l DebugLogger) Log(format string, a ...interface{}) {
+	golog.Printf(format, a...)
+}
+
+func SetLogger(l Logger) {
+	if l != nil {
+		log = l
+	}
+}

+ 28 - 0
overlay.go

@@ -0,0 +1,28 @@
+package vessel
+
+import (
+	"net/http"
+	"os"
+)
+
+type OverlayFS struct {
+	search []http.FileSystem
+}
+
+func NewOverlayFS() *OverlayFS {
+	return &OverlayFS{}
+}
+
+func (f *OverlayFS) Append(fs http.FileSystem) {
+	f.search = append(f.search, fs)
+}
+
+func (f *OverlayFS) Open(name string) (http.File, error) {
+	for _, source := range f.search {
+		file, err := source.Open(name)
+		if err == nil {
+			return file, err
+		}
+	}
+	return nil, os.ErrNotExist
+}

+ 24 - 7
vessel.go

@@ -3,7 +3,6 @@ package vessel
 import (
 	"fmt"
 	"io/ioutil"
-	"log"
 	"os"
 )
 
@@ -27,11 +26,20 @@ func Open(src string) (*Bundle, error) {
 }
 
 // Create attaches a src file to a dest to create a vessel bundle
-func Create(dst string, src string) (*Bundle, error) {
+func Create(dst, src string, overWrite bool) (*Bundle, error) {
 	b, err := Open(dst)
 	if err == nil {
+		truncateAt := b.offset
 		b.Close()
-		return nil, fmt.Errorf("already a bundle")
+
+		if !overWrite {
+			return nil, fmt.Errorf("already a bundle")
+		}
+
+		err = os.Truncate(dst, truncateAt)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	err = concatenate(dst, src)
@@ -43,20 +51,29 @@ func Create(dst string, src string) (*Bundle, error) {
 }
 
 // Create attaches a src file to a dest to create a vessel bundle
-func Build(dst string, src string) (*Bundle, error) {
+func Build(dst, src string, overWrite bool, filter func(file string) bool) (*Bundle, error) {
 	b, err := Open(dst)
 	if err == nil {
+		truncateAt := b.offset
 		b.Close()
-		return nil, fmt.Errorf("already a bundle")
+
+		if !overWrite {
+			return nil, fmt.Errorf("already a bundle")
+		}
+
+		err = os.Truncate(dst, truncateAt)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	file, err := ioutil.TempFile("./", "build.zip")
 	if err != nil {
-		log.Fatal(err)
+		return nil, err
 	}
 	defer os.Remove(file.Name())
 
-	err = packInto(file, src)
+	err = packInto(file, src, filter)
 	if err != nil {
 		return nil, err
 	}