113 lines
2.9 KiB
Go
113 lines
2.9 KiB
Go
package barkfile
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"time"
|
|
)
|
|
|
|
const defaultPollInterval = 250 * time.Millisecond
|
|
|
|
// Change is one validated Barkfile revision or an error reading or validating it.
|
|
// Changes are delivered in source order and include the initial valid revision.
|
|
type Change struct {
|
|
Config Config
|
|
Err error
|
|
}
|
|
|
|
// WatchOptions configures file watching. A zero PollInterval uses 250ms.
|
|
type WatchOptions struct {
|
|
PollInterval time.Duration
|
|
}
|
|
|
|
// Load reads, parses, and validates a Barkfile.
|
|
func Load(path string) (Config, error) {
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, fmt.Errorf("read Barkfile: %w", err)
|
|
}
|
|
return parseAndValidate(contents)
|
|
}
|
|
|
|
// Watch emits the initial valid Barkfile and each subsequent file-content change.
|
|
// An invalid or unreadable revision is delivered as Change.Err; watching continues
|
|
// so a later valid revision is delivered. Cancel ctx to close the returned channel.
|
|
func Watch(ctx context.Context, path string, options WatchOptions) (<-chan Change, error) {
|
|
if ctx == nil {
|
|
return nil, errors.New("watch context is required")
|
|
}
|
|
interval := options.PollInterval
|
|
if interval == 0 {
|
|
interval = defaultPollInterval
|
|
}
|
|
if interval < 0 {
|
|
return nil, errors.New("watch poll interval cannot be negative")
|
|
}
|
|
|
|
initial, fingerprint, err := loadRevision(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
changes := make(chan Change)
|
|
go watchLoop(ctx, path, interval, initial, fingerprint, changes)
|
|
return changes, nil
|
|
}
|
|
|
|
func watchLoop(ctx context.Context, path string, interval time.Duration, initial Config, fingerprint [sha256.Size]byte, changes chan<- Change) {
|
|
defer close(changes)
|
|
if !sendChange(ctx, changes, Change{Config: initial}) {
|
|
return
|
|
}
|
|
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
return
|
|
case <-ticker.C:
|
|
config, nextFingerprint, err := loadRevision(path)
|
|
if nextFingerprint == fingerprint {
|
|
continue
|
|
}
|
|
fingerprint = nextFingerprint
|
|
if !sendChange(ctx, changes, Change{Config: config, Err: err}) {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func sendChange(ctx context.Context, changes chan<- Change, change Change) bool {
|
|
select {
|
|
case changes <- change:
|
|
return true
|
|
case <-ctx.Done():
|
|
return false
|
|
}
|
|
}
|
|
|
|
func loadRevision(path string) (Config, [sha256.Size]byte, error) {
|
|
contents, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return Config{}, sha256.Sum256([]byte("read error:\x00" + err.Error())), fmt.Errorf("read Barkfile: %w", err)
|
|
}
|
|
fingerprint := sha256.Sum256(contents)
|
|
config, err := parseAndValidate(contents)
|
|
return config, fingerprint, err
|
|
}
|
|
|
|
func parseAndValidate(contents []byte) (Config, error) {
|
|
config, err := Parse(contents)
|
|
if err != nil {
|
|
return Config{}, err
|
|
}
|
|
if err := config.Validate(); err != nil {
|
|
return Config{}, fmt.Errorf("invalid Barkfile: %w", err)
|
|
}
|
|
return config, nil
|
|
}
|