add caching support
This commit is contained in:
parent
8400387d2a
commit
989bb18482
98
cache.go
Normal file
98
cache.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"hash/crc32"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fileCache struct {
|
||||||
|
baseDir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fileCache) retrieveFile(context *Context, outputPath string, inputFiles []*File) (*File, error) {
|
||||||
|
cachePath, err := c.buildCachePath(context, outputPath, inputFiles)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outputFile, err := context.CreateFileFromAsset(outputPath, cachePath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fileCache) storeFile(context *Context, outputFile *File, inputFiles []*File) error {
|
||||||
|
cachePath, err := c.buildCachePath(context, outputFile.Path(), inputFiles)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(c.baseDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
fp, err := os.Create(cachePath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fp.Close()
|
||||||
|
|
||||||
|
offset, err := outputFile.Seek(0, os.SEEK_CUR)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := outputFile.Seek(0, os.SEEK_SET); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := outputFile.WriteTo(fp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := outputFile.Seek(offset, os.SEEK_SET); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *fileCache) buildCachePath(context *Context, outputPath string, inputFiles []*File) (string, error) {
|
||||||
|
uintBuff := make([]byte, 4)
|
||||||
|
binary.LittleEndian.PutUint32(uintBuff, context.hash)
|
||||||
|
|
||||||
|
hasher := crc32.NewIEEE()
|
||||||
|
hasher.Write(uintBuff)
|
||||||
|
hasher.Write([]byte(outputPath))
|
||||||
|
|
||||||
|
sort.Sort(FilesByPath(inputFiles))
|
||||||
|
for _, inputFile := range inputFiles {
|
||||||
|
fileHash, err := inputFile.hash()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
binary.LittleEndian.PutUint32(uintBuff, fileHash)
|
||||||
|
|
||||||
|
hasher.Write(uintBuff)
|
||||||
|
hasher.Write([]byte(inputFile.Path()))
|
||||||
|
}
|
||||||
|
|
||||||
|
cachePath := filepath.Join(c.baseDir, fmt.Sprintf(
|
||||||
|
"gs_%.8x%s",
|
||||||
|
hasher.Sum32(),
|
||||||
|
filepath.Ext(outputPath),
|
||||||
|
))
|
||||||
|
|
||||||
|
return cachePath, nil
|
||||||
|
}
|
111
context.go
111
context.go
@ -1,61 +1,110 @@
|
|||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type context struct {
|
type Context struct {
|
||||||
gs *goldsmith
|
goldsmith *Goldsmith
|
||||||
plug Plugin
|
|
||||||
filters []Filter
|
plugin Plugin
|
||||||
input, output chan *file
|
hash uint32
|
||||||
|
|
||||||
|
fileFilters []Filter
|
||||||
|
inputFiles chan *File
|
||||||
|
outputFiles chan *File
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *context) step() {
|
func (*Context) CreateFileFromData(sourcePath string, data []byte) *File {
|
||||||
defer close(ctx.output)
|
return &File{
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
Meta: make(map[string]interface{}),
|
||||||
|
reader: bytes.NewReader(data),
|
||||||
|
size: int64(len(data)),
|
||||||
|
modTime: time.Now(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Context) CreateFileFromAsset(sourcePath, dataPath string) (*File, error) {
|
||||||
|
info, err := os.Stat(dataPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil, errors.New("assets must be files")
|
||||||
|
}
|
||||||
|
|
||||||
|
file := &File{
|
||||||
|
sourcePath: sourcePath,
|
||||||
|
dataPath: dataPath,
|
||||||
|
Meta: make(map[string]interface{}),
|
||||||
|
size: info.Size(),
|
||||||
|
modTime: info.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return file, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) DispatchFile(file *File) {
|
||||||
|
ctx.outputFiles <- file
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) {
|
||||||
|
ctx.goldsmith.storeFile(ctx, outputFile, inputFiles)
|
||||||
|
ctx.outputFiles <- outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File {
|
||||||
|
return ctx.goldsmith.retrieveFile(ctx, outputPath, inputFiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *Context) step() {
|
||||||
|
defer close(ctx.outputFiles)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
var filters []Filter
|
var filters []Filter
|
||||||
if initializer, ok := ctx.plug.(Initializer); ok {
|
if initializer, ok := ctx.plugin.(Initializer); ok {
|
||||||
filters, err = initializer.Initialize(ctx)
|
filters, err = initializer.Initialize(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.gs.fault(ctx.plug.Name(), nil, err)
|
ctx.goldsmith.fault(ctx.plugin.Name(), nil, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ctx.input != nil {
|
if ctx.inputFiles != nil {
|
||||||
processor, _ := ctx.plug.(Processor)
|
processor, _ := ctx.plugin.(Processor)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
for i := 0; i < runtime.NumCPU(); i++ {
|
for i := 0; i < runtime.NumCPU(); i++ {
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for f := range ctx.input {
|
for inputFile := range ctx.inputFiles {
|
||||||
accept := processor != nil
|
accept := processor != nil
|
||||||
for _, filter := range append(ctx.filters, filters...) {
|
for _, filter := range append(ctx.fileFilters, filters...) {
|
||||||
if accept, err = filter.Accept(ctx, f); err != nil {
|
if accept, err = filter.Accept(ctx, inputFile); err != nil {
|
||||||
ctx.gs.fault(filter.Name(), f, err)
|
ctx.goldsmith.fault(filter.Name(), inputFile, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if !accept {
|
if !accept {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if accept {
|
if accept {
|
||||||
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
if _, err := inputFile.Seek(0, os.SEEK_SET); err != nil {
|
||||||
ctx.gs.fault("core", f, err)
|
ctx.goldsmith.fault("core", inputFile, err)
|
||||||
}
|
}
|
||||||
if err := processor.Process(ctx, f); err != nil {
|
if err := processor.Process(ctx, inputFile); err != nil {
|
||||||
ctx.gs.fault(ctx.plug.Name(), f, err)
|
ctx.goldsmith.fault(ctx.plugin.Name(), inputFile, err)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.output <- f
|
ctx.outputFiles <- inputFile
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@ -63,25 +112,9 @@ func (ctx *context) step() {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
if finalizer, ok := ctx.plug.(Finalizer); ok {
|
if finalizer, ok := ctx.plugin.(Finalizer); ok {
|
||||||
if err := finalizer.Finalize(ctx); err != nil {
|
if err := finalizer.Finalize(ctx); err != nil {
|
||||||
ctx.gs.fault(ctx.plug.Name(), nil, err)
|
ctx.goldsmith.fault(ctx.plugin.Name(), nil, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Context Implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
func (ctx *context) DispatchFile(f File) {
|
|
||||||
ctx.output <- f.(*file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *context) SrcDir() string {
|
|
||||||
return ctx.gs.srcDir
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ctx *context) DstDir() string {
|
|
||||||
return ctx.gs.dstDir
|
|
||||||
}
|
|
||||||
|
132
core.go
132
core.go
@ -1,132 +0,0 @@
|
|||||||
package goldsmith
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
)
|
|
||||||
|
|
||||||
type goldsmith struct {
|
|
||||||
srcDir, dstDir string
|
|
||||||
contexts []*context
|
|
||||||
refs map[string]bool
|
|
||||||
complete bool
|
|
||||||
filters []Filter
|
|
||||||
|
|
||||||
errors []error
|
|
||||||
errorMtx sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) pushContext(plug Plugin) *context {
|
|
||||||
ctx := &context{gs: gs, plug: plug, output: make(chan *file)}
|
|
||||||
ctx.filters = append(ctx.filters, gs.filters...)
|
|
||||||
|
|
||||||
if len(gs.contexts) > 0 {
|
|
||||||
ctx.input = gs.contexts[len(gs.contexts)-1].output
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.contexts = append(gs.contexts, ctx)
|
|
||||||
return ctx
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) cleanupFiles() {
|
|
||||||
infos := make(chan fileInfo)
|
|
||||||
go scanDir(gs.dstDir, infos)
|
|
||||||
|
|
||||||
for info := range infos {
|
|
||||||
relPath, _ := filepath.Rel(gs.dstDir, info.path)
|
|
||||||
if contained, _ := gs.refs[relPath]; contained {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
os.RemoveAll(info.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) exportFile(f *file) error {
|
|
||||||
if err := f.export(gs.dstDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pathSeg := cleanPath(f.path)
|
|
||||||
for {
|
|
||||||
gs.refs[pathSeg] = true
|
|
||||||
if pathSeg == "." {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
pathSeg = filepath.Dir(pathSeg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) fault(name string, f *file, err error) {
|
|
||||||
gs.errorMtx.Lock()
|
|
||||||
defer gs.errorMtx.Unlock()
|
|
||||||
|
|
||||||
ferr := &Error{Name: name, Err: err}
|
|
||||||
if f != nil {
|
|
||||||
ferr.Path = f.path
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.errors = append(gs.errors, ferr)
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// Goldsmith Implementation
|
|
||||||
//
|
|
||||||
|
|
||||||
func (gs *goldsmith) Chain(p Plugin) Goldsmith {
|
|
||||||
if gs.complete {
|
|
||||||
panic("attempted reuse of goldsmith instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.pushContext(p)
|
|
||||||
return gs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) FilterPush(f Filter) Goldsmith {
|
|
||||||
if gs.complete {
|
|
||||||
panic("attempted reuse of goldsmith instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.filters = append(gs.filters, f)
|
|
||||||
return gs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) FilterPop() Goldsmith {
|
|
||||||
if gs.complete {
|
|
||||||
panic("attempted reuse of goldsmith instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
count := len(gs.filters)
|
|
||||||
if count == 0 {
|
|
||||||
panic("attempted to pop empty filter stack")
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.filters = gs.filters[:count-1]
|
|
||||||
return gs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (gs *goldsmith) End(dstDir string) []error {
|
|
||||||
if gs.complete {
|
|
||||||
panic("attempted reuse of goldsmith instance")
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.dstDir = dstDir
|
|
||||||
|
|
||||||
for _, ctx := range gs.contexts {
|
|
||||||
go ctx.step()
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := gs.contexts[len(gs.contexts)-1]
|
|
||||||
for f := range ctx.output {
|
|
||||||
gs.exportFile(f)
|
|
||||||
}
|
|
||||||
|
|
||||||
gs.cleanupFiles()
|
|
||||||
gs.complete = true
|
|
||||||
|
|
||||||
return gs.errors
|
|
||||||
}
|
|
18
error.go
Normal file
18
error.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type Error struct {
|
||||||
|
Name string
|
||||||
|
Path string
|
||||||
|
Err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e Error) Error() string {
|
||||||
|
var path string
|
||||||
|
if len(e.Path) > 0 {
|
||||||
|
path = "@" + e.Path
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("[%s%s]: %s", e.Name, path, e.Err.Error())
|
||||||
|
}
|
206
file.go
206
file.go
@ -2,46 +2,114 @@ package goldsmith
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"hash/crc32"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type file struct {
|
type File struct {
|
||||||
path string
|
sourcePath string
|
||||||
|
dataPath string
|
||||||
|
|
||||||
Meta map[string]interface{}
|
Meta map[string]interface{}
|
||||||
|
|
||||||
|
hashValue uint32
|
||||||
|
hashValid bool
|
||||||
|
|
||||||
reader *bytes.Reader
|
reader *bytes.Reader
|
||||||
size int64
|
size int64
|
||||||
modTime time.Time
|
modTime time.Time
|
||||||
|
|
||||||
asset string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *file) export(dstDir string) error {
|
func (f *File) Path() string {
|
||||||
dstPath := filepath.Join(dstDir, f.path)
|
return f.sourcePath
|
||||||
if len(f.asset) > 0 {
|
}
|
||||||
dstInfo, err := os.Stat(dstPath)
|
|
||||||
if err == nil && dstInfo.ModTime().Unix() >= f.ModTime().Unix() {
|
func (f *File) Name() string {
|
||||||
return nil
|
return path.Base(f.sourcePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *File) Dir() string {
|
||||||
|
return path.Dir(f.sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Ext() string {
|
||||||
|
return path.Ext(f.sourcePath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Size() int64 {
|
||||||
|
return f.size
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) ModTime() time.Time {
|
||||||
|
return f.modTime
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Read(data []byte) (int, error) {
|
||||||
|
if err := f.load(); err != nil {
|
||||||
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.MkdirAll(path.Dir(dstPath), 0755); err != nil {
|
return f.reader.Read(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) WriteTo(writer io.Writer) (int64, error) {
|
||||||
|
if err := f.load(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.reader.WriteTo(writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) Seek(offset int64, whence int) (int64, error) {
|
||||||
|
if f.reader == nil && offset == 0 && (whence == os.SEEK_SET || whence == os.SEEK_CUR) {
|
||||||
|
return 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := f.load(); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.reader.Seek(offset, whence)
|
||||||
|
}
|
||||||
|
|
||||||
|
type FilesByPath []*File
|
||||||
|
|
||||||
|
func (f FilesByPath) Len() int {
|
||||||
|
return len(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilesByPath) Swap(i, j int) {
|
||||||
|
f[i], f[j] = f[j], f[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f FilesByPath) Less(i, j int) bool {
|
||||||
|
return strings.Compare(f[i].Path(), f[j].Path()) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *File) export(targetDir string) error {
|
||||||
|
targetPath := filepath.Join(targetDir, f.sourcePath)
|
||||||
|
if targetInfo, err := os.Stat(targetPath); err == nil && targetInfo.ModTime().After(f.ModTime()) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(path.Dir(targetPath), 0755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, err := os.Create(dstPath)
|
fw, err := os.Create(targetPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer fw.Close()
|
defer fw.Close()
|
||||||
|
|
||||||
if f.reader == nil {
|
if f.reader == nil {
|
||||||
fr, err := os.Open(f.asset)
|
fr, err := os.Open(f.dataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -54,6 +122,7 @@ func (f *file) export(dstDir string) error {
|
|||||||
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, err := f.WriteTo(fw); err != nil {
|
if _, err := f.WriteTo(fw); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -62,12 +131,12 @@ func (f *file) export(dstDir string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *file) cache() error {
|
func (f *File) load() error {
|
||||||
if f.reader != nil {
|
if f.reader != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := ioutil.ReadFile(f.asset)
|
data, err := ioutil.ReadFile(f.dataPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -76,73 +145,62 @@ func (f *file) cache() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
func (f *File) hash() (uint32, error) {
|
||||||
// File Implementation
|
if f.hashValid {
|
||||||
//
|
return f.hashValue, nil
|
||||||
|
|
||||||
func (f *file) Path() string {
|
|
||||||
return f.path
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Name() string {
|
|
||||||
return path.Base(f.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Dir() string {
|
|
||||||
return path.Dir(f.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Ext() string {
|
|
||||||
return path.Ext(f.path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Size() int64 {
|
|
||||||
return f.size
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) ModTime() time.Time {
|
|
||||||
return f.modTime
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Value(key string) (interface{}, bool) {
|
|
||||||
return getDelimValue(f.Meta, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) SetValue(key string, value interface{}) bool {
|
|
||||||
return setDelimValue(f.Meta, key, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) InheritValues(src File) {
|
|
||||||
rf := src.(*file)
|
|
||||||
for name, value := range rf.Meta {
|
|
||||||
f.SetValue(name, value)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Read(p []byte) (int, error) {
|
if err := f.load(); err != nil {
|
||||||
if err := f.cache(); err != nil {
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.reader.Read(p)
|
offset, err := f.Seek(0, os.SEEK_CUR)
|
||||||
}
|
if err != nil {
|
||||||
|
|
||||||
func (f *file) WriteTo(w io.Writer) (int64, error) {
|
|
||||||
if err := f.cache(); err != nil {
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.reader.WriteTo(w)
|
if _, err := f.Seek(0, os.SEEK_SET); err != nil {
|
||||||
}
|
|
||||||
|
|
||||||
func (f *file) Seek(offset int64, whence int) (int64, error) {
|
|
||||||
if f.reader == nil && offset == 0 && (whence == os.SEEK_SET || whence == os.SEEK_CUR) {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.cache(); err != nil {
|
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return f.reader.Seek(offset, whence)
|
hasher := crc32.NewIEEE()
|
||||||
|
if _, err := io.Copy(hasher, f.reader); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := f.Seek(offset, os.SEEK_SET); err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f.hashValue = hasher.Sum32()
|
||||||
|
f.hashValid = true
|
||||||
|
return f.hashValue, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type fileInfo struct {
|
||||||
|
os.FileInfo
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanPath(path string) string {
|
||||||
|
if filepath.IsAbs(path) {
|
||||||
|
var err error
|
||||||
|
if path, err = filepath.Rel("/", path); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Clean(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scanDir(rootDir string, infos chan fileInfo) {
|
||||||
|
defer close(infos)
|
||||||
|
|
||||||
|
filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err == nil {
|
||||||
|
infos <- fileInfo{FileInfo: info, path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
214
goldsmith.go
214
goldsmith.go
@ -1,118 +1,144 @@
|
|||||||
package goldsmith
|
package goldsmith
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"hash"
|
||||||
"errors"
|
"hash/crc32"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Goldsmith interface {
|
type Goldsmith struct {
|
||||||
Chain(p Plugin) Goldsmith
|
sourceDir string
|
||||||
FilterPush(f Filter) Goldsmith
|
targetDir string
|
||||||
FilterPop() Goldsmith
|
|
||||||
End(dstDir string) []error
|
contexts []*Context
|
||||||
|
contextHasher hash.Hash32
|
||||||
|
|
||||||
|
fileRefs map[string]bool
|
||||||
|
fileFilters []Filter
|
||||||
|
fileCache *fileCache
|
||||||
|
|
||||||
|
errors []error
|
||||||
|
errorMtx sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func Begin(srcDir string) Goldsmith {
|
func Begin(sourceDir string) *Goldsmith {
|
||||||
gs := &goldsmith{srcDir: srcDir, refs: make(map[string]bool)}
|
gs := &Goldsmith{
|
||||||
|
sourceDir: sourceDir,
|
||||||
|
contextHasher: crc32.NewIEEE(),
|
||||||
|
fileRefs: make(map[string]bool),
|
||||||
|
}
|
||||||
|
|
||||||
gs.Chain(new(loader))
|
gs.Chain(new(loader))
|
||||||
return gs
|
return gs
|
||||||
}
|
}
|
||||||
|
|
||||||
type File interface {
|
func (gs *Goldsmith) Cache(cacheDir string) *Goldsmith {
|
||||||
Path() string
|
gs.fileCache = &fileCache{cacheDir}
|
||||||
Name() string
|
return gs
|
||||||
Dir() string
|
|
||||||
Ext() string
|
|
||||||
Size() int64
|
|
||||||
ModTime() time.Time
|
|
||||||
|
|
||||||
Value(key string) (interface{}, bool)
|
|
||||||
SetValue(key string, value interface{}) bool
|
|
||||||
InheritValues(src File)
|
|
||||||
|
|
||||||
Read(p []byte) (int, error)
|
|
||||||
WriteTo(w io.Writer) (int64, error)
|
|
||||||
Seek(offset int64, whence int) (int64, error)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileFromData(path string, data []byte, modTime time.Time) File {
|
func (gs *Goldsmith) Chain(plugin Plugin) *Goldsmith {
|
||||||
return &file{
|
gs.contextHasher.Write([]byte(plugin.Name()))
|
||||||
path: path,
|
|
||||||
Meta: make(map[string]interface{}),
|
context := &Context{
|
||||||
reader: bytes.NewReader(data),
|
goldsmith: gs,
|
||||||
size: int64(len(data)),
|
plugin: plugin,
|
||||||
modTime: modTime,
|
hash: gs.contextHasher.Sum32(),
|
||||||
|
outputFiles: make(chan *File),
|
||||||
|
}
|
||||||
|
|
||||||
|
context.fileFilters = append(context.fileFilters, gs.fileFilters...)
|
||||||
|
|
||||||
|
if len(gs.contexts) > 0 {
|
||||||
|
context.inputFiles = gs.contexts[len(gs.contexts)-1].outputFiles
|
||||||
|
}
|
||||||
|
|
||||||
|
gs.contexts = append(gs.contexts, context)
|
||||||
|
return gs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) FilterPush(filter Filter) *Goldsmith {
|
||||||
|
gs.fileFilters = append(gs.fileFilters, filter)
|
||||||
|
return gs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) FilterPop() *Goldsmith {
|
||||||
|
count := len(gs.fileFilters)
|
||||||
|
if count == 0 {
|
||||||
|
panic("attempted to pop empty filter stack")
|
||||||
|
}
|
||||||
|
|
||||||
|
gs.fileFilters = gs.fileFilters[:count-1]
|
||||||
|
return gs
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) End(targetDir string) []error {
|
||||||
|
gs.targetDir = targetDir
|
||||||
|
|
||||||
|
for _, context := range gs.contexts {
|
||||||
|
go context.step()
|
||||||
|
}
|
||||||
|
|
||||||
|
context := gs.contexts[len(gs.contexts)-1]
|
||||||
|
for file := range context.outputFiles {
|
||||||
|
gs.exportFile(file)
|
||||||
|
}
|
||||||
|
|
||||||
|
gs.removeUnreferencedFiles()
|
||||||
|
return gs.errors
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) retrieveFile(context *Context, outputPath string, inputFiles []*File) *File {
|
||||||
|
if gs.fileCache != nil {
|
||||||
|
outputFile, _ := gs.fileCache.retrieveFile(context, outputPath, inputFiles)
|
||||||
|
return outputFile
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) storeFile(context *Context, outputFile *File, inputFiles []*File) {
|
||||||
|
if gs.fileCache != nil {
|
||||||
|
gs.fileCache.storeFile(context, outputFile, inputFiles)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewFileFromAsset(path, asset string) (File, error) {
|
func (gs *Goldsmith) removeUnreferencedFiles() {
|
||||||
info, err := os.Stat(asset)
|
infos := make(chan fileInfo)
|
||||||
if err != nil {
|
go scanDir(gs.targetDir, infos)
|
||||||
return nil, err
|
|
||||||
|
for info := range infos {
|
||||||
|
if info.path != gs.targetDir {
|
||||||
|
relPath, _ := filepath.Rel(gs.targetDir, info.path)
|
||||||
|
if contained, _ := gs.fileRefs[relPath]; !contained {
|
||||||
|
os.RemoveAll(info.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gs *Goldsmith) exportFile(file *File) error {
|
||||||
|
if err := file.export(gs.targetDir); err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if info.IsDir() {
|
for pathSeg := cleanPath(file.sourcePath); pathSeg != "."; pathSeg = filepath.Dir(pathSeg) {
|
||||||
return nil, errors.New("assets must be files")
|
gs.fileRefs[pathSeg] = true
|
||||||
}
|
}
|
||||||
|
|
||||||
f := &file{
|
return nil
|
||||||
path: path,
|
}
|
||||||
Meta: make(map[string]interface{}),
|
|
||||||
size: info.Size(),
|
func (gs *Goldsmith) fault(pluginName string, file *File, err error) {
|
||||||
modTime: info.ModTime(),
|
gs.errorMtx.Lock()
|
||||||
asset: asset,
|
defer gs.errorMtx.Unlock()
|
||||||
|
|
||||||
|
faultError := &Error{Name: pluginName, Err: err}
|
||||||
|
if file != nil {
|
||||||
|
faultError.Path = file.sourcePath
|
||||||
}
|
}
|
||||||
|
|
||||||
return f, nil
|
gs.errors = append(gs.errors, faultError)
|
||||||
}
|
|
||||||
|
|
||||||
type Context interface {
|
|
||||||
DispatchFile(f File)
|
|
||||||
|
|
||||||
SrcDir() string
|
|
||||||
DstDir() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Name string
|
|
||||||
Path string
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e Error) Error() string {
|
|
||||||
var path string
|
|
||||||
if len(e.Path) > 0 {
|
|
||||||
path = "@" + e.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("[%s%s]: %s", e.Name, path, e.Err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
type Initializer interface {
|
|
||||||
Initialize(ctx Context) ([]Filter, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Processor interface {
|
|
||||||
Process(ctx Context, f File) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Finalizer interface {
|
|
||||||
Finalize(ctx Context) error
|
|
||||||
}
|
|
||||||
|
|
||||||
type Component interface {
|
|
||||||
Name() string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Filter interface {
|
|
||||||
Component
|
|
||||||
Accept(ctx Context, f File) (bool, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type Plugin interface {
|
|
||||||
Component
|
|
||||||
}
|
}
|
||||||
|
26
interface.go
Normal file
26
interface.go
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
package goldsmith
|
||||||
|
|
||||||
|
type Initializer interface {
|
||||||
|
Initialize(ctx *Context) ([]Filter, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Processor interface {
|
||||||
|
Process(ctx *Context, file *File) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Finalizer interface {
|
||||||
|
Finalize(ctx *Context) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Component interface {
|
||||||
|
Name() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Filter interface {
|
||||||
|
Component
|
||||||
|
Accept(ctx *Context, file *File) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Plugin interface {
|
||||||
|
Component
|
||||||
|
}
|
20
loader.go
20
loader.go
@ -8,26 +8,26 @@ func (*loader) Name() string {
|
|||||||
return "loader"
|
return "loader"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*loader) Initialize(ctx Context) ([]Filter, error) {
|
func (*loader) Initialize(ctx *Context) ([]Filter, error) {
|
||||||
infos := make(chan fileInfo)
|
infos := make(chan fileInfo)
|
||||||
go scanDir(ctx.SrcDir(), infos)
|
go scanDir(ctx.goldsmith.sourceDir, infos)
|
||||||
|
|
||||||
for info := range infos {
|
for info := range infos {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
relPath, _ := filepath.Rel(ctx.SrcDir(), info.path)
|
relPath, _ := filepath.Rel(ctx.goldsmith.sourceDir, info.path)
|
||||||
|
|
||||||
f := &file{
|
file := &File{
|
||||||
path: relPath,
|
sourcePath: relPath,
|
||||||
Meta: make(map[string]interface{}),
|
Meta: make(map[string]interface{}),
|
||||||
modTime: info.ModTime(),
|
modTime: info.ModTime(),
|
||||||
size: info.Size(),
|
size: info.Size(),
|
||||||
asset: info.path,
|
dataPath: info.path,
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.DispatchFile(f)
|
ctx.DispatchFile(file)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
101
util.go
101
util.go
@ -1,101 +0,0 @@
|
|||||||
package goldsmith
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fileInfo struct {
|
|
||||||
os.FileInfo
|
|
||||||
path string
|
|
||||||
}
|
|
||||||
|
|
||||||
func cleanPath(path string) string {
|
|
||||||
if filepath.IsAbs(path) {
|
|
||||||
var err error
|
|
||||||
if path, err = filepath.Rel("/", path); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Clean(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
func scanDir(root string, infos chan fileInfo) {
|
|
||||||
defer close(infos)
|
|
||||||
|
|
||||||
filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
infos <- fileInfo{FileInfo: info, path: path}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func setDelimValue(container interface{}, path string, data interface{}) bool {
|
|
||||||
containerVal := reflect.Indirect(reflect.ValueOf(container))
|
|
||||||
|
|
||||||
segments := strings.Split(path, ".")
|
|
||||||
segmentHead := segments[0]
|
|
||||||
|
|
||||||
if len(segments) > 1 {
|
|
||||||
var fieldVal reflect.Value
|
|
||||||
switch containerVal.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
fieldVal = containerVal.MapIndex(reflect.ValueOf(segmentHead))
|
|
||||||
case reflect.Struct:
|
|
||||||
fieldVal = containerVal.FieldByName(segmentHead)
|
|
||||||
if fieldVal.CanAddr() {
|
|
||||||
fieldVal = fieldVal.Addr()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldVal.IsValid() && fieldVal.CanInterface() {
|
|
||||||
pathRest := strings.Join(segments[1:], ".")
|
|
||||||
return setDelimValue(fieldVal.Interface(), pathRest, data)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
switch containerVal.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
containerVal.SetMapIndex(reflect.ValueOf(segmentHead), reflect.ValueOf(data))
|
|
||||||
return true
|
|
||||||
case reflect.Struct:
|
|
||||||
fieldVal := containerVal.FieldByName(segmentHead)
|
|
||||||
if fieldVal.CanSet() {
|
|
||||||
fieldVal.Set(reflect.ValueOf(data))
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDelimValue(container interface{}, path string) (interface{}, bool) {
|
|
||||||
containerVal := reflect.Indirect(reflect.ValueOf(container))
|
|
||||||
|
|
||||||
segments := strings.Split(path, ".")
|
|
||||||
segmentHead := segments[0]
|
|
||||||
|
|
||||||
var fieldVal reflect.Value
|
|
||||||
switch containerVal.Kind() {
|
|
||||||
case reflect.Map:
|
|
||||||
fieldVal = containerVal.MapIndex(reflect.ValueOf(segmentHead))
|
|
||||||
case reflect.Struct:
|
|
||||||
fieldVal = containerVal.FieldByName(segmentHead)
|
|
||||||
}
|
|
||||||
|
|
||||||
if fieldVal.IsValid() && fieldVal.CanInterface() {
|
|
||||||
if len(segments) > 1 {
|
|
||||||
return getDelimValue(fieldVal.Interface(), strings.Join(segments[1:], "."))
|
|
||||||
}
|
|
||||||
|
|
||||||
return fieldVal.Interface(), true
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, false
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user