Skip to content

Commit

Permalink
Merge pull request #4 from inabyte/add_writefile
Browse files Browse the repository at this point in the history
Add WriteFile
  • Loading branch information
MylesCagney committed Feb 5, 2020
2 parents 0ede1e6 + 0ed87ac commit 2abeffa
Show file tree
Hide file tree
Showing 16 changed files with 1,114 additions and 1,046 deletions.
5 changes: 4 additions & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ go:
- 1.13.x
- tip

before_install:
- go get github.com/mattn/goveralls

script:
- go test -v -gcflags=-l ./... -race -coverprofile=coverage.txt -covermode=atomic

after_success:
- bash <(curl -s https://codecov.io/bash)
- goveralls -v -service travis-ci -coverprofile=coverage.txt

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Embed static files into go binaries

[![Build Status](https://travis-ci.com/inabyte/embed.svg?branch=master)](https://travis-ci.com/inabyte/embed)
[![Coverage Status](https://coveralls.io/repos/github/inabyte/embed/badge.svg?branch=master)](https://coveralls.io/github/inabyte/embed?branch=master)

Takes a list for file or folders (likely at `go generate` time) and
generates Go code that statically implements the a http.FileSystem.
Expand Down
76 changes: 74 additions & 2 deletions embedded/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import (
"os"
"path"
"path/filepath"
"sort"
"strings"
"time"
"unsafe"
)

// FileSystem defines the FileSystem interface and builder
Expand All @@ -37,6 +39,11 @@ type FileSystem interface {
// AddFolder add a file to embedded filesystem
AddFolder(path string, name string, local string, modtime int64, paths ...string)

// WriteFile writes data to a file named by filename.
// If the file does not exist, WriteFile creates it with permissions perm;
// otherwise WriteFile truncates it before writing.
WriteFile(filename string, data []byte, perm os.FileMode) error

// UseLocal use on disk copy instead of embedded data (for development)
UseLocal(bool)
}
Expand Down Expand Up @@ -93,8 +100,8 @@ type files struct {
}

// New creates a new Files that loads embedded content.
func New() FileSystem {
return &files{list: make(map[string]*file)}
func New(count int) FileSystem {
return &files{list: make(map[string]*file, count)}
}

func (fs *files) Open(name string) (file http.File, err error) {
Expand Down Expand Up @@ -228,6 +235,71 @@ func (fs *files) AddFolder(path string, name string, local string, modtime int64
}
}

func (fs *files) addToFolder(filename string) (err error) {
folder := path.Dir(filename)

if f, ok := fs.list[folder]; !ok {
fs.list[folder] = &file{
name: path.Base(folder),
isDir: true,
modtime: time.Now().Unix(),
}
err = fs.addToFolder(folder)
if err != nil {
delete(fs.list, folder)
}
} else {
if !f.isDir {
err = &os.PathError{Op: "mkdir", Path: folder, Err: os.ErrInvalid}
}
}

if err == nil {
f := fs.list[folder]
f.subFiles = append(f.subFiles, fs.list[filename])
if len(f.subFiles) > 1 {
sort.Slice(f.subFiles, func(i, j int) bool {
return strings.Compare(f.subFiles[i].Name(), f.subFiles[j].Name()) == -1
})
}
}

return
}

func (fs *files) WriteFile(filename string, data []byte, perm os.FileMode) (err error) {

// Make a copy of the byte data
local := make([]byte, len(data))
copy(local, data)
localStr := *(*string)(unsafe.Pointer(&local))

// If file exists just replace the data
if f, ok := fs.list[filename]; ok {
f.tag = ""
f.mimeType = ""
f.size = int64(len(local))
f.compressed = false
f.data = local
f.str = localStr
} else {
fs.list[filename] = &file{
name: path.Base(filename),
size: int64(len(local)),
modtime: time.Now().Unix(),
data: local,
str: localStr,
}

// Now add folder entries as required
if err = fs.addToFolder(filename); err != nil {
delete(fs.list, filename)
}
}

return
}

func (fs *files) UseLocal(value bool) {
fs.local = value
}
Expand Down
51 changes: 49 additions & 2 deletions embedded/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,52 @@ var (
indexTag = tag(indexBytes)
)

func TestWriteFile(t *testing.T) {
dir, f := makeFs()
defer os.RemoveAll(dir)

for _, test := range []struct {
name string
file string
hasError bool
data []byte
}{
{"Replace", "/settings.html", false, []byte("some data")},
{"Add", "/images/picture.svg", false, []byte("some image data")},
{"Bad Path", "/settings.html/js/utils.js", true, []byte("some script data")},
} {
t.Run(test.name, func(t *testing.T) {

err := f.WriteFile(test.file, test.data, os.ModePerm)

if err == nil {
if test.hasError {
t.Errorf("Test %s did not error as expected", test.name)
}
} else {
if !test.hasError {
t.Errorf("Test %s returned unexpected error %v", test.name, err)
}
}

if !test.hasError {
if file, err := f.Open(test.file); err != nil {
t.Errorf("Test %s open file returned unexpected error %v", test.name, err)
} else {
if b, err := ioutil.ReadAll(file); err != nil {
t.Errorf("Test %s ReadAll returned unexpected error %v", test.name, err)
} else {
if !reflect.DeepEqual(b, test.data) {
t.Errorf("Did not get expected contents (%s) expect (%s)", b, test.data)
}
}
}

}
})
}
}

func TestWalk(t *testing.T) {
dir, f := makeFs()
defer os.RemoveAll(dir)
Expand All @@ -56,10 +102,11 @@ func TestWalk(t *testing.T) {
})

if !reflect.DeepEqual(list, test.expect) {
t.Errorf("Did not wlak file as expected got(%v) expect (%v)", list, test.expect)
t.Errorf("Did not Walk file as expected got(%v) expect (%v)", list, test.expect)
}
})
}

}

func TestFiles(t *testing.T) {
Expand Down Expand Up @@ -180,7 +227,7 @@ func TestFiles(t *testing.T) {
func makeFs() (string, FileSystem) {
tmpdir, _ := ioutil.TempDir("", "fs-test")

f := New()
f := New(6)

f.AddFile("/index.html", "index.html",
filepath.Join(tmpdir, "index.html"),
Expand Down
14 changes: 11 additions & 3 deletions embedded/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,17 +168,25 @@ func localRedirect(w http.ResponseWriter, r *http.Request, newPath string) {
// serve set various headers etag, content type
func (f *reader) serve(w http.ResponseWriter, r *http.Request) {
tag := f.tag

// Check is requesting compressed and we have it compressed
if f.compressed && strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
w.Header().Set("Content-Encoding", "gzip")
f.readCompressed = true
f.length = int64(len(f.data))
} else {
tag = tag[:len(tag)-3]
if len(tag) > 3 {
tag = tag[:len(tag)-3]
}
}

w.Header().Set("Content-Type", f.mimeType)
w.Header().Set("Etag", strconv.Quote(tag))
if len(f.mimeType) > 0 {
w.Header().Set("Content-Type", f.mimeType)
}

if len(tag) > 0 {
w.Header().Set("Etag", strconv.Quote(tag))
}

// ServeContent will check modification time
http.ServeContent(w, r, f.Name(), f.ModTime(), f)
Expand Down
75 changes: 43 additions & 32 deletions file.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"crypto/sha1"
"encoding/base64"
"fmt"
"io/ioutil"
"mime"
"net/http"
"os"
Expand All @@ -26,14 +27,14 @@ import (
type file struct {
name string
baseName string
data []byte
local string
Size int64
Size int
ModTime int64
mimeType string
tag string
dataSize int
Compressed bool
Offset int64
offset int

fileinfo os.FileInfo
}
Expand All @@ -59,12 +60,8 @@ func (f *file) set() {
stringer.add(f.tag)
}

func (f *file) Data() []byte {
return f.data
}

func (f *file) Slice() string {
return fmt.Sprintf("%d:%d", f.Offset, f.Offset+int64(len(f.data)))
return fmt.Sprintf("%d:%d", f.offset, f.offset+f.dataSize)
}

func (f *file) Name() string {
Expand All @@ -86,47 +83,61 @@ func (f *file) Tag() string {
return stringer.slice(f.tag)
}

func (f *file) setMimeType() {
f.mimeType = mime.TypeByExtension(filepath.Ext(f.name))
if f.mimeType == "" {
// read a chunk to decide between utf-8 text and binary
f.mimeType = http.DetectContentType(f.data)
}
}
func (f *file) write(w writer) error {
var (
buf bytes.Buffer
gw *gzip.Writer
)

func (f *file) fill() {
hash := sha1.Sum(f.data)
f.tag = base64.RawURLEncoding.EncodeToString(hash[:]) + "-gz"
f.Size = int64(len(f.data))
}
f.offset = w.offset()
b, err := ioutil.ReadFile(f.local)

if err == nil {
// Determine mimetype
f.mimeType = mime.TypeByExtension(filepath.Ext(f.name))
if f.mimeType == "" {
// read a chunk to decide between utf-8 text and binary
f.mimeType = http.DetectContentType(b)
}

// Minify the data
if m, e := minifier.Bytes(f.mimeType, b); e == nil {
b = m
}

func (f *file) compress() error {
var buf = &bytes.Buffer{}
// Create eTag
hash := sha1.Sum(b)
f.tag = base64.RawURLEncoding.EncodeToString(hash[:]) + "-gz"

gw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
f.Size = len(b)
f.dataSize = f.Size

gw, err = gzip.NewWriterLevel(&buf, gzip.BestCompression)
}

if err == nil {
_, err = gw.Write(f.data)
_, err = gw.Write(b)
}

if err == nil {
err = gw.Close()
}

if err == nil {
if buf.Len() < len(f.data) {
f.data = buf.Bytes()
if buf.Len() < f.Size {
b = buf.Bytes()
f.dataSize = len(b)
f.Compressed = true
}
}

return err
}

func (f *file) minify() {
if b, err := minifier.Bytes(f.mimeType, f.data); err == nil {
f.data = b
if err == nil {
f.dataSize, err = w.Write(b)
}

f.set()

return err
}

func (d *dir) set() {
Expand Down
Loading

0 comments on commit 2abeffa

Please sign in to comment.