package projects import ( "fmt" "io/fs" "os" "sync" "time" "github.com/pterm/pterm" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/gitlab" "gopkg.in/yaml.v3" ) type Cache struct { Projects []*gitlab.Project Aliases []*ProjectAlias Updated time.Time readFromFile bool lock *sync.Mutex ttl time.Duration file string log *pterm.Logger gitlab *gitlab.Client } type CacheOpts struct { Path string TTL time.Duration Logger *pterm.Logger Gitlab *gitlab.Client } // Load cache, if already loaded and up to date, nothing is done. // If the cache is not yet loaded from disk, it is loaded // If the updated timestamp is beyond the set ttl, a refresh is triggered 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 } // Saves the current state of the cache to disk 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", c.log.Args("error", err)) } d := yaml.NewEncoder(file) if err = d.Encode(*c); err != nil { c.log.Error("Failed to Marshal cache to yaml", c.log.Args("error", err)) } else { c.log.Debug("Cache saved to disk") } } func (c *Cache) Write() { c.lock.Lock() defer c.lock.Unlock() c.write() } // Loads and unmarshals the project cache from disk. func (c *Cache) Read() { c.lock.Lock() defer c.lock.Unlock() c.log.Debug("Reading project cache from disk", c.log.Args("file", c.file)) file, err := os.Open(c.file) if err != nil { c.log.Error("Failed to read project cache", c.log.Args("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()) } // Resets projects cache and also optionally clears // project aliase cache. Writes to disk. func (c *Cache) clear(clearAliases bool) { c.log.Info("Clearing project cache") c.Projects = make([]*gitlab.Project, 0) if clearAliases { c.log.Info("Clearing project alias cache") c.Aliases = make([]*ProjectAlias, 0) } c.setUpdated() c.log.Debug(c.String()) } func (c *Cache) Clear(clearAliases bool) { c.lock.Lock() defer c.lock.Unlock() c.clear(clearAliases) } func (c *Cache) refresh() { c.log.Info("Refreshing project cache, this may take a while") defer c.setUpdated() c.LoadProjects() } // Iterates through all GitLab projects the user has access to, updating // the project cache where necessary 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.Project, 0), Aliases: make([]*ProjectAlias, 0), file: opts.Path, ttl: opts.TTL, lock: &sync.Mutex{}, log: opts.Logger, gitlab: opts.Gitlab, } return cache, err } func createProjectCache(path string) error { return os.WriteFile(path, nil, 0640) }