package setup import ( "context" "errors" "fmt" "io" "os" "path/filepath" "github.com/safedep/dry/log" "github.com/safedep/pmg/analyzer/malysiscache" "github.com/safedep/pmg/config" "github.com/safedep/pmg/internal/localstore" "github.com/spf13/cobra" ) // NewCacheCommand returns the `pmg setup cache` command tree. func NewCacheCommand() *cobra.Command { cmd := &cobra.Command{ Use: "cache", Short: "Inspect and clear PMG's persistent analysis cache", RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, } cmd.AddCommand(newCacheStatusCommand()) cmd.AddCommand(newCacheClearCommand()) return cmd } func newCacheStatusCommand() *cobra.Command { return &cobra.Command{ Use: "status", Short: "Show analysis cache path, state, TTL, and entry count", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { return runCacheStatus(cmd.Context(), config.Get(), os.Stdout) }, } } func newCacheClearCommand() *cobra.Command { return &cobra.Command{ Use: "clear", Short: "Delete all cached analysis verdicts", SilenceUsage: true, RunE: func(cmd *cobra.Command, _ []string) error { return runCacheClear(cmd.Context(), config.Get(), os.Stdout) }, } } // openCache opens the shared localdb and the malysis cache module. exists is // false when the DB file is absent (nothing cached yet); the caller decides how // to report that without creating the file. func openCache(ctx context.Context, cfg *config.RuntimeConfig) (cache *malysiscache.Cache, closeFn func(), exists bool, err error) { dbPath := filepath.Join(cfg.LocalDBDir(), cfg.LocalDBFileName()) if _, statErr := os.Stat(dbPath); statErr != nil { if errors.Is(statErr, os.ErrNotExist) { return nil, func() {}, false, nil } return nil, func() {}, false, statErr } mgr := localstore.NewManager(cfg) store, serr := mgr.Store(ctx, malysiscache.Descriptor()) if serr != nil { if cerr := mgr.Close(); cerr != nil { log.Warnf("failed to close localdb: %v", cerr) } return nil, func() {}, true, serr } closeFn = func() { if cerr := mgr.Close(); cerr != nil { log.Warnf("failed to close localdb: %v", cerr) } } return malysiscache.New(store, cfg.Config.AnalysisCache.Malysis), closeFn, true, nil } func runCacheStatus(ctx context.Context, cfg *config.RuntimeConfig, out io.Writer) error { mc := cfg.Config.AnalysisCache.Malysis dbPath := filepath.Join(cfg.LocalDBDir(), cfg.LocalDBFileName()) cache, closeFn, exists, err := openCache(ctx, cfg) if err != nil { return fmt.Errorf("open analysis cache: %w", err) } defer closeFn() count := 0 if exists { stats, serr := cache.Stats(ctx) if serr != nil { return fmt.Errorf("read analysis cache: %w", serr) } count = stats.Count } if _, err := fmt.Fprintf(out, "Path: %s\nEnabled: %v\nTTL: %s\nEntries: %d\n", dbPath, mc.Enabled, mc.TTL, count); err != nil { return err } return nil } func runCacheClear(ctx context.Context, cfg *config.RuntimeConfig, out io.Writer) error { cache, closeFn, exists, err := openCache(ctx, cfg) if err != nil { return fmt.Errorf("open analysis cache: %w", err) } defer closeFn() if !exists { if _, werr := fmt.Fprintln(out, "Analysis cache is already empty."); werr != nil { return werr } return nil } if err := cache.Clear(ctx); err != nil { return fmt.Errorf("clear analysis cache: %w", err) } if _, werr := fmt.Fprintln(out, "Analysis cache cleared."); werr != nil { return werr } return nil }