package main import ( "fmt" "os" "sync" ) var filesystem FileSystem = NewRealFileSystem() // FileSystem abstracts filesystem operations so we can swap implementations type FileSystem interface { Remove(path string) error RemoveAll(path string) error MkdirAll(path string, perm os.FileMode) error Symlink(source, target string) error Link(source, target string) error SummaryLines() []string IsDryRun() bool } const ( opRemove = "remove" opRemoveAll = "remove_all" opMkdirAll = "mkdir_all" opSymlink = "symlink" opHardlink = "hardlink" ) type operationRecord struct { kind string source string target string err error dryRun bool } func (r operationRecord) summaryLine() (string, bool) { if r.kind != opSymlink && r.kind != opHardlink { return "", false } kindLabel := "Symlink" if r.kind == opHardlink { kindLabel = "Hardlink" } status := "OK" if r.err != nil { status = fmt.Sprintf("FAIL: %v", r.err) } else if r.dryRun { status = "DRY-RUN" } return fmt.Sprintf("%s %s -> %s (%s)", kindLabel, r.source, r.target, status), true } type baseFileSystem struct { mu sync.Mutex operations []operationRecord } func (b *baseFileSystem) addOperation(kind, source, target string, err error, dryRun bool) { b.mu.Lock() defer b.mu.Unlock() b.operations = append(b.operations, operationRecord{ kind: kind, source: source, target: target, err: err, dryRun: dryRun, }) } func (b *baseFileSystem) snapshot() []operationRecord { b.mu.Lock() defer b.mu.Unlock() ops := make([]operationRecord, len(b.operations)) copy(ops, b.operations) return ops } func summarizeOperations(records []operationRecord) []string { var lines []string for _, record := range records { if line, ok := record.summaryLine(); ok { lines = append(lines, line) } } return lines } type realFileSystem struct { baseFileSystem } // NewRealFileSystem returns a filesystem implementation that writes to disk func NewRealFileSystem() FileSystem { return &realFileSystem{} } func (fs *realFileSystem) Remove(path string) error { err := os.Remove(path) fs.addOperation(opRemove, "", path, err, false) return err } func (fs *realFileSystem) RemoveAll(path string) error { err := os.RemoveAll(path) fs.addOperation(opRemoveAll, "", path, err, false) return err } func (fs *realFileSystem) MkdirAll(path string, perm os.FileMode) error { err := os.MkdirAll(path, perm) fs.addOperation(opMkdirAll, "", path, err, false) return err } func (fs *realFileSystem) Symlink(source, target string) error { err := os.Symlink(source, target) fs.addOperation(opSymlink, source, target, err, false) return err } func (fs *realFileSystem) Link(source, target string) error { err := os.Link(source, target) fs.addOperation(opHardlink, source, target, err, false) return err } func (fs *realFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } func (fs *realFileSystem) IsDryRun() bool { return false } type dryRunFileSystem struct { baseFileSystem } // NewDryRunFileSystem returns a filesystem implementation that only records operations func NewDryRunFileSystem() FileSystem { return &dryRunFileSystem{} } func (fs *dryRunFileSystem) Remove(path string) error { fs.addOperation(opRemove, "", path, nil, true) return nil } func (fs *dryRunFileSystem) RemoveAll(path string) error { fs.addOperation(opRemoveAll, "", path, nil, true) return nil } func (fs *dryRunFileSystem) MkdirAll(path string, perm os.FileMode) error { fs.addOperation(opMkdirAll, "", path, nil, true) return nil } func (fs *dryRunFileSystem) Symlink(source, target string) error { fs.addOperation(opSymlink, source, target, nil, true) return nil } func (fs *dryRunFileSystem) Link(source, target string) error { fs.addOperation(opHardlink, source, target, nil, true) return nil } func (fs *dryRunFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } func (fs *dryRunFileSystem) IsDryRun() bool { return true }