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  }