package projects import ( "fmt" "os" "strings" "sync" "time" "github.com/pterm/pterm" "gitlab.sweetwater.com/it/devops/tools/gitlab-project-manager/internal/config" "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 config *config.Config readFromFile bool lock *sync.Mutex ttl time.Duration file string log *pterm.Logger gitlab *gitlab.Client path string } type CacheOpts struct { Path string ProjectsPath string TTL time.Duration Logger *pterm.Logger Gitlab *gitlab.Client Config *config.Config } // 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|os.O_CREATE|os.O_TRUNC, 0640) 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() error { 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 err } d := yaml.NewDecoder(file) d.Decode(c) c.readFromFile = true return nil } // 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() } func (c *Cache) Clear(clearAliases bool) { c.lock.Lock() defer c.lock.Unlock() c.clear(clearAliases) } func (c *Cache) refresh() { c.log.Info("Loading project cache, this may take a while\n") 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)) } // This command will only dump projects that have // been cloned locally. Setting all to true will list all projects func (c *Cache) DumpString(all bool) string { str := strings.Builder{} var term string if all { term = pterm.Bold.Sprint("Projects") } else { term = pterm.Bold.Sprint("Local Projects") } str.WriteString(c.String() + "\n\n" + term + ":\n") for _, project := range c.Projects { if !all && !c.IsProjectCloned(project) { continue } str.WriteString(" - " + pterm.FgLightBlue.Sprint(project.Name) + " (") str.WriteString(project.PathWithNamespace + ")\n") aliases := c.GetProjectAliases(project) if len(aliases) > 0 { str.WriteString(pterm.FgLightGreen.Sprint(" aliases:")) for _, a := range aliases { str.WriteString(" [" + pterm.FgCyan.Sprint(a.Alias) + "]") } str.WriteRune('\n') } } return str.String() } 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), config: opts.Config, file: opts.Path, ttl: opts.TTL, lock: &sync.Mutex{}, log: opts.Logger, gitlab: opts.Gitlab, path: opts.ProjectsPath, } return cache, err } func createProjectCache(path string) error { return os.WriteFile(path, nil, 0640) }