package goldsmith import ( "bytes" "errors" "io" "os" "path/filepath" "runtime" "sync" "time" ) // Context corresponds to the current link in the chain and provides methods // that enable plugins to inject new files into the chain. type Context struct { chain *chainState plugin Plugin filtersExt filterStack filtersInt filterStack threads int index int filesIn chan *File filesOut chan *File } // CreateFileFrom data creates a new file instance from the provided data buffer. func (self *Context) CreateFileFromReader(relPath string, reader io.Reader) (*File, error) { if filepath.IsAbs(relPath) { return nil, errors.New("file paths must be relative") } data, err := io.ReadAll(reader) if err != nil { return nil, err } file := &File{ relPath: relPath, props: make(FileProps), modTime: time.Now(), size: int64(len(data)), reader: bytes.NewReader(data), index: self.index, } return file, nil } // CreateFileFromAsset creates a new file instance from the provided file path. func (self *Context) CreateFileFromAsset(relPath, dataPath string) (*File, error) { if filepath.IsAbs(relPath) { return nil, errors.New("file paths must be relative") } info, err := os.Stat(dataPath) if err != nil { return nil, err } if info.IsDir() { return nil, errors.New("file paths cannot be directories") } file := &File{ relPath: relPath, props: make(FileProps), modTime: info.ModTime(), size: info.Size(), dataPath: dataPath, index: self.index, } return file, nil } // DispatchFile causes the file to get passed to the next link in the chain. func (self *Context) DispatchFile(file *File) { self.filesOut <- file } // DispatchAndCacheFile caches the file data (excluding the metadata), taking // dependencies on any input files that are needed to generate it, and then // passes it to the next link in the chain. func (self *Context) DispatchAndCacheFile(outputFile *File, inputFiles ...*File) { if self.chain.cache != nil { self.chain.cache.storeFile(outputFile, inputFiles) } self.DispatchFile(outputFile) } // RetrieveCachedFile looks up file data (excluding the metadata), given an // output path and any input files that are needed to generate it. The function // will return nil if the desired file is not found in the cache. func (self *Context) RetrieveCachedFile(outputPath string, inputFiles ...*File) *File { var outputFile *File if self.chain.cache != nil { outputFile, _ = self.chain.cache.retrieveFile(self, outputPath, inputFiles) } return outputFile } // Specify internal filter(s) that exclude files from being processed. func (self *Context) Filter(filters ...Filter) *Context { for _, filter := range filters { self.filtersInt.push(filter, self.index) } return self } // Specify the maximum number of threads used for processing. func (self *Context) Threads(threads int) *Context { self.threads = threads return self } func (self *Context) step() { defer close(self.filesOut) if initializer, ok := self.plugin.(Initializer); ok { if err := initializer.Initialize(self); err != nil { self.chain.fault(self.plugin.Name(), nil, err) return } } if self.filesIn != nil { processor, _ := self.plugin.(Processor) threads := self.threads if threads < 1 { threads = runtime.NumCPU() } var wg sync.WaitGroup for i := 0; i < threads; i++ { wg.Add(1) go func() { defer wg.Done() for inputFile := range self.filesIn { if processor != nil && self.filtersInt.accept(inputFile) && self.filtersExt.accept(inputFile) { if _, err := inputFile.Seek(0, io.SeekStart); err != nil { self.chain.fault("core", inputFile, err) } if err := processor.Process(self, inputFile); err != nil { self.chain.fault(self.plugin.Name(), inputFile, err) } } else { self.DispatchFile(inputFile) } } }() } wg.Wait() } if finalizer, ok := self.plugin.(Finalizer); ok { if err := finalizer.Finalize(self); err != nil { self.chain.fault(self.plugin.Name(), nil, err) } } }