package projects import ( "fmt" "io/fs" "os" "sync" "time" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" "golang.org/x/exp/slog" "gopkg.in/yaml.v3" ) type Cache struct { Projects []*gitlab.ProjectInfo Aliases []*gitlab.ProjectAlias Updated time.Time readFromFile bool lock *sync.Mutex ttl time.Duration file string log *slog.Logger } type CacheOpts struct { Path string TTL time.Duration Logger *slog.Logger } // Load cache, if already loaded and // up to date, func (c *Cache) Load() error { var err error if !c.readFromFile { c.Read() } if time.Since(c.Updated) > c.ttl { c.log.Info("Project cache stale, updating.") c.Refresh() } return err } func (c *Cache) Write() { file, err := os.OpenFile(c.file, os.O_RDWR, fs.ModeAppend) if err != nil { c.log.Error("Failed to write cache to disk", "error", err) } d := yaml.NewEncoder(file) if err = d.Encode(*c); err != nil { c.log.Error("Failed to Marshal cache to yaml", "error", err) } else { c.log.Info("Cache saved to disk") } } func (c *Cache) Read() { c.lock.Lock() defer c.lock.Unlock() c.log.Info("Reading project cache from disk", "file", c.file) file, err := os.Open(c.file) if err != nil { c.log.Error("Failed to read project cache", "error", err) return } d := yaml.NewDecoder(file) d.Decode(c) if time.Since(c.Updated) > c.ttl { c.refresh() } c.readFromFile = true c.log.Debug(c.String()) } func (c *Cache) refresh() { c.log.Info("Refreshing project cache, this may take a while") defer c.setUpdated() } func (c *Cache) Refresh() { c.lock.Lock() defer c.lock.Unlock() c.refresh() } func (c *Cache) String() string { return fmt.Sprintf("Cache Updated %s: Projects %d, Aliases %d", c.Updated.String(), len(c.Projects), len(c.Aliases)) } func (c *Cache) setUpdated() { c.Updated = time.Now() } func (c *Cache) SetUpdated() { c.lock.Lock() defer c.lock.Unlock() c.setUpdated() } func (c *Cache) GetUpdated() time.Time { return c.Updated } // Returns a new project cache ready to load from // cache file. // Cache.Load() must be called manually func NewProjectCache(opts *CacheOpts) (*Cache, error) { var err error if _, err = os.Stat(opts.Path); err != nil { err = createProjectCache(opts.Path) } cache := &Cache{ Projects: make([]*gitlab.ProjectInfo, 0), Aliases: make([]*gitlab.ProjectAlias, 0), file: opts.Path, ttl: opts.TTL, lock: &sync.Mutex{}, log: opts.Logger, } return cache, err } func createProjectCache(path string) error { return os.WriteFile(path, nil, 0640) }