mf4
Raw Download raw file
  1package extractor
  2
  3import (
  4	"context"
  5	"errors"
  6	"fmt"
  7	"io/fs"
  8	"log/slog"
  9	"os"
 10	"path/filepath"
 11	"slices"
 12
 13	"github.com/aymanbagabas/go-udiff"
 14)
 15
 16var (
 17	ErrFileModified error = errors.New("dst file modified")
 18)
 19
 20const (
 21	_statusWalkError = "walk_error"
 22	_statusDir       = "directory"
 23	_statusFailed    = "failed"
 24	_statusCreated   = "created"
 25	_statusUnchanged = "unchanged"
 26	_statusModified  = "modified"
 27)
 28
 29type ExtractResult struct {
 30	Src     string
 31	Dst     string
 32	Status  string
 33	Details string
 34	Err     error
 35}
 36
 37type Extractor struct {
 38	dst     string          // target dir
 39	fs      fs.FS           // source fs (e.g. embed.FS)
 40	mode    os.FileMode     // apply to all files
 41	Results []ExtractResult // per-file results
 42}
 43
 44// TODO: Options
 45func New(
 46	fs fs.FS,
 47	dst string,
 48) (*Extractor, error) {
 49
 50	mode := os.FileMode(0o700)
 51	return &Extractor{
 52		dst:  dst,
 53		fs:   fs,
 54		mode: mode,
 55	}, nil
 56}
 57
 58func (e *Extractor) Deploy() error {
 59	return fs.WalkDir(e.fs, ".", e.deployFile)
 60}
 61
 62// Deploy extracts an the embedded extractor.fs filesystem into the exractor.dst
 63// directory. Embeded paths will be preserved.
 64//   - implements the fs.WalkDirFunc interface
 65func (e *Extractor) deployFile(path string, d fs.DirEntry, err error) error {
 66
 67	dir, _ := filepath.Split(path)
 68	dstPath := filepath.Join(e.dst, path)
 69	dstDir := filepath.Join(e.dst, dir)
 70	result := ExtractResult{
 71		Src: path,
 72		Dst: dstPath,
 73	}
 74
 75	defer func() {
 76		e.Results = append(e.Results, result)
 77	}()
 78
 79	if err != nil {
 80		result.Err = err
 81		result.Status = _statusWalkError
 82		return nil
 83	}
 84
 85	if d.IsDir() {
 86		result.Status = _statusDir
 87		return nil
 88	}
 89
 90	dstExists := true
 91	stat, err := os.Stat(dstPath)
 92	if err != nil {
 93		if !errors.Is(err, os.ErrNotExist) {
 94			result.Err = fmt.Errorf("failed to stat file: %w", err)
 95			return nil
 96		}
 97		dstExists = false
 98	}
 99	if stat != nil && stat.IsDir() {
100		result.Status = _statusDir
101		return nil
102	}
103
104	err = os.MkdirAll(dstDir, e.mode)
105	if err != nil {
106		result.Status = _statusFailed
107		result.Err = fmt.Errorf("failed to create dir: %w", err)
108		return nil
109	}
110
111	// TODO: Reader / Writer for big files
112	srcData, err := fs.ReadFile(e.fs, path)
113	if err != nil {
114		result.Status = _statusFailed
115		result.Err = fmt.Errorf("failed to read src file: %w", err)
116		return nil
117	}
118
119	if dstExists {
120		dstData, err := os.ReadFile(dstPath)
121		if err != nil {
122			result.Status = _statusFailed
123			result.Err = fmt.Errorf("failed to read dst file: %w", err)
124			return nil
125		}
126
127		if slices.Equal(srcData, dstData) {
128			result.Status = _statusUnchanged
129			return nil
130		}
131
132		diff := udiff.Unified(path, dstPath, string(srcData), string(dstData))
133		result.Status = _statusModified
134		result.Details = diff
135		result.Err = ErrFileModified
136		return nil
137	}
138
139	err = os.WriteFile(dstPath, srcData, e.mode)
140	if err != nil {
141		result.Status = _statusFailed
142		result.Err = fmt.Errorf("failed to write dst: %w", err)
143		return nil
144	}
145	return nil
146}
147
148func (e *Extractor) LogResults(logger *slog.Logger, ctx context.Context) {
149	for _, r := range e.Results {
150		level := slog.LevelInfo
151		if r.Status == _statusDir || r.Status == _statusUnchanged {
152			level = slog.LevelDebug
153		}
154		attrs := []slog.Attr{
155			slog.String("status", r.Status),
156			slog.String("dst", r.Dst),
157		}
158		if r.Err != nil {
159			level = slog.LevelError
160			if errors.Is(r.Err, ErrFileModified) {
161				level = slog.LevelWarn
162			}
163			attrs = append(attrs, slog.Any("err", r.Err))
164		}
165		logger.LogAttrs(ctx, level, r.Status, attrs...)
166	}
167}