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 RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) SummaryLines() []string SummaryRecords() []operationRecord 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) { detail := r.detail() if detail != "" { return fmt.Sprintf("%s %s → %s (%s) %s", r.kindLabel(), r.formattedSource(), r.formattedTarget(), r.resultLabel(), detail), true } return fmt.Sprintf("%s %s → %s (%s)", r.kindLabel(), r.formattedSource(), r.formattedTarget(), r.resultLabel()), true } func (r operationRecord) kindLabel() string { switch r.kind { case opSymlink: return "Symlink" case opHardlink: return "Hardlink" case opRemove: return "Remove" case opRemoveAll: return "RemoveAll" case opMkdirAll: return "MkdirAll" default: return r.kind } } func (r operationRecord) resultLabel() string { if r.err != nil { return fmt.Sprintf("%sFAIL%s", BRed, Reset) } if r.dryRun { return fmt.Sprintf("%sDRY-RUN%s", BCyan, Reset) } return fmt.Sprintf("%sOK%s", BGreen, Reset) } func (r operationRecord) formattedSource() string { if r.source == "" { return "-" } return FormatSourcePath(r.source) } func (r operationRecord) formattedTarget() string { if r.target == "" { return "-" } return FormatTargetPath(r.target) } func (r operationRecord) detail() string { if r.err != nil { return r.err.Error() } if r.dryRun { return "dry-run" } return "" } 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.RecordLinkAttempt(opSymlink, source, target, err, false) return err } func (fs *realFileSystem) Link(source, target string) error { err := os.Link(source, target) fs.RecordLinkAttempt(opHardlink, source, target, err, false) return err } func (fs *realFileSystem) RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) { fs.addOperation(kind, source, target, err, dryRun) } func (fs *realFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } func (fs *realFileSystem) SummaryRecords() []operationRecord { return 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.RecordLinkAttempt(opSymlink, source, target, nil, true) return nil } func (fs *dryRunFileSystem) Link(source, target string) error { fs.RecordLinkAttempt(opHardlink, source, target, nil, true) return nil } func (fs *dryRunFileSystem) RecordLinkAttempt(kind string, source, target string, err error, dryRun bool) { fs.addOperation(kind, source, target, err, dryRun) } func (fs *dryRunFileSystem) SummaryLines() []string { return summarizeOperations(fs.snapshot()) } func (fs *dryRunFileSystem) SummaryRecords() []operationRecord { return fs.snapshot() } func (fs *dryRunFileSystem) IsDryRun() bool { return true }