github.com/sudo-bmitch/version-bump@v0.0.0-20240503123857-70b0e3f646dd/internal/lockfile/lockfile.go (about) 1 // Package lockfile is used to manage the lockfile of managed versions 2 package lockfile 3 4 import ( 5 "encoding/json" 6 "errors" 7 "fmt" 8 "io" 9 "os" 10 "path/filepath" 11 "sort" 12 "sync" 13 14 "golang.org/x/exp/maps" 15 ) 16 17 // Lock stores known versions from a scan or source 18 type Lock struct { 19 Name string `json:"name"` 20 Key string `json:"key"` 21 Version string `json:"version"` 22 } 23 24 type Locks struct { 25 mu sync.Mutex 26 Filename string 27 Lock map[string]map[string]*Lock // Lock[Name][Key] = *Lock 28 } 29 30 func New() *Locks { 31 return &Locks{ 32 Lock: map[string]map[string]*Lock{}, 33 } 34 } 35 36 func (l *Locks) Get(name, key string) (*Lock, error) { 37 l.mu.Lock() 38 defer l.mu.Unlock() 39 if _, ok := l.Lock[name]; !ok { 40 return nil, fmt.Errorf("not found") 41 } 42 entry, ok := l.Lock[name][key] 43 if !ok { 44 return nil, fmt.Errorf("not found") 45 } 46 return entry, nil 47 } 48 49 func (l *Locks) Set(name, key, version string) error { 50 l.mu.Lock() 51 defer l.mu.Unlock() 52 if _, ok := l.Lock[name]; !ok { 53 l.Lock[name] = map[string]*Lock{} 54 } 55 l.Lock[name][key] = &Lock{ 56 Name: name, 57 Key: key, 58 Version: version, 59 } 60 return nil 61 } 62 63 func LoadReader(rdr io.Reader) (*Locks, error) { 64 decode := json.NewDecoder(rdr) 65 l := New() 66 var err error 67 for { 68 entry := &Lock{} 69 err = decode.Decode(&entry) 70 if err != nil { 71 break 72 } 73 if _, ok := l.Lock[entry.Name]; !ok { 74 l.Lock[entry.Name] = map[string]*Lock{} 75 } 76 l.Lock[entry.Name][entry.Key] = entry 77 } 78 if !errors.Is(err, io.EOF) { 79 return nil, fmt.Errorf("failed to read lock file: %w", err) 80 } 81 82 return l, nil 83 } 84 85 func LoadFile(filename string) (*Locks, error) { 86 fh, err := os.Open(filename) 87 if err != nil { 88 return nil, fmt.Errorf("failed to open lock file %s: %w", filename, err) 89 } 90 defer fh.Close() 91 return LoadReader(fh) 92 } 93 94 func (l *Locks) Save() error { 95 return SaveFile(l.Filename, l) 96 } 97 98 func SaveWriter(write io.Writer, l *Locks) error { 99 if l == nil || l.Lock == nil { 100 return fmt.Errorf("cannot save nil lockfile") 101 } 102 // sort to keep the file deterministic 103 names := maps.Keys(l.Lock) 104 sort.Strings(names) 105 for _, name := range names { 106 keys := maps.Keys(l.Lock[name]) 107 sort.Strings(keys) 108 for _, key := range keys { 109 if err := json.NewEncoder(write).Encode(l.Lock[name][key]); err != nil { 110 return fmt.Errorf("failed to encode lockfile content: %w", err) 111 } 112 } 113 } 114 return nil 115 } 116 117 func SaveFile(filename string, l *Locks) error { 118 // write to a temp file 119 dir := filepath.Dir(filename) 120 if err := os.MkdirAll(dir, 0755); err != nil { 121 return fmt.Errorf("failed to create %s: %w", dir, err) 122 } 123 tmp, err := os.CreateTemp(dir, filepath.Base(filename)) 124 if err != nil { 125 return fmt.Errorf("unable to create temp file in %s: %w", dir, err) 126 } 127 tmpName := tmp.Name() 128 err = SaveWriter(tmp, l) 129 tmp.Close() 130 defer func() { 131 if err != nil { 132 os.Remove(tmpName) 133 } 134 }() 135 if err != nil { 136 return fmt.Errorf("failed to save lock file %s: %w", tmpName, err) 137 } 138 // update permissions to match existing file or 0644 139 mode := os.FileMode(0644) 140 stat, err := os.Stat(filename) 141 if err == nil && stat.Mode().IsRegular() { 142 mode = stat.Mode() 143 } 144 if err := os.Chmod(tmpName, mode); err != nil { 145 return fmt.Errorf("failed to change permission on lockfile %s: %w", tmpName, err) 146 } 147 // move temp file to target filename 148 if err := os.Rename(tmpName, filename); err != nil { 149 return fmt.Errorf("failed to replace lockfile %s: %w", filename, err) 150 } 151 return nil 152 }