From 84cf8c2c2b89df4024b7186d04438512506c8357 Mon Sep 17 00:00:00 2001 From: henning mueller Date: Fri, 1 Apr 2022 21:28:00 +0200 Subject: [PATCH] Add `compatdata purge` command --- README.md | 1 + cmd/protonutils/compatdata.go | 108 ++++++++++++++++++++++++++++------ steam/LibraryConfig.go | 16 +++++ steam/compatdata.go | 43 ++++++++++++++ utils/dir.go | 25 ++++++++ 5 files changed, 174 insertions(+), 19 deletions(-) create mode 100644 steam/compatdata.go create mode 100644 utils/dir.go diff --git a/README.md b/README.md index 79d2839..949086a 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ protonutils is a CLI tool that provides different utilities to make using the [P * Print or open compatdata and install directories by game name (handy of you want to mess with savegames or mods, for example) * Update assigned compatibility tool for one or more games +* Clean-up unused `compatdata` directories The commands `list`, `appid`, `compatdata`, `installdir`, and `compattool` do only work with (non-native) games that either have an explicit Proton/CompatTool mapping or have been started at least once with Proton. diff --git a/cmd/protonutils/compatdata.go b/cmd/protonutils/compatdata.go index 120fc6f..6501d18 100644 --- a/cmd/protonutils/compatdata.go +++ b/cmd/protonutils/compatdata.go @@ -1,13 +1,14 @@ package main import ( - "errors" "fmt" + "os" "os/exec" - "path" "strings" + "github.com/dustin/go-humanize" "github.com/nning/protonutils/steam" + "github.com/nning/protonutils/utils" "github.com/spf13/cobra" ) @@ -32,6 +33,14 @@ var compatdataOpenCmd = &cobra.Command{ Run: compatdataOpen, } +var compatdataPurgeCmd = &cobra.Command{ + Use: "purge", + Short: "Purge unused compatdata directories", + Long: "Purge leftover compatdata directories of previously installed and now uninstalled games", + Args: cobra.MinimumNArgs(0), + Run: compatdataPurge, +} + func init() { rootCmd.AddCommand(compatdataCmd) @@ -44,27 +53,18 @@ func init() { compatdataOpenCmd.Flags().StringVarP(&user, "user", "u", "", "Steam user name (or SteamID3)") compatdataOpenCmd.Flags().BoolVarP(&ignoreCache, "ignore-cache", "c", false, "Ignore app ID/name cache") compatdataOpenCmd.Flags().BoolVarP(&verbose, "verbose", "v", false, "Show app name") + + compatdataCmd.AddCommand(compatdataPurgeCmd) + compatdataPurgeCmd.Flags().StringVarP(&user, "user", "u", "", "Steam user name (or SteamID3)") + compatdataPurgeCmd.Flags().BoolVarP(&ignoreCache, "ignore-cache", "c", false, "Ignore app ID/name cache") + compatdataPurgeCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Do not ask") } -func getCompatdataPath(idOrName string) (string, string, error) { +func compatdataPath(cmd *cobra.Command, args []string) { s, err := steam.New(user, cfg.SteamRoot, ignoreCache) exitOnError(err) - id, name, err := s.GetAppIDAndName(idOrName) - if err != nil { - return "", "", err - } - - p := s.LibraryConfig.GetLibraryPathByID(id) - if p == "" { - exitOnError(errors.New("Game not installed")) - } - - return path.Join(p, "steamapps", "compatdata", id), name, nil -} - -func compatdataPath(cmd *cobra.Command, args []string) { - p, n, err := getCompatdataPath(strings.Join(args, " ")) + p, n, err := s.GetCompatdataPath(strings.Join(args, " ")) exitOnAmbiguousNameError(cmd, args, err) if verbose { @@ -75,7 +75,10 @@ func compatdataPath(cmd *cobra.Command, args []string) { } func compatdataOpen(cmd *cobra.Command, args []string) { - p, n, err := getCompatdataPath(strings.Join(args, " ")) + s, err := steam.New(user, cfg.SteamRoot, ignoreCache) + exitOnError(err) + + p, n, err := s.GetCompatdataPath(strings.Join(args, " ")) exitOnAmbiguousNameError(cmd, args, err) if verbose { @@ -85,3 +88,70 @@ func compatdataOpen(cmd *cobra.Command, args []string) { _, err = exec.Command("xdg-open", p).Output() exitOnError(err) } + +func compatdataPurge(cmd *cobra.Command, args []string) { + s, err := steam.New(user, cfg.SteamRoot, ignoreCache) + exitOnError(err) + + err = s.ReadCompatTools() + exitOnError(err) + + fmt.Println("Calculating unused compatdata directory sizes...") + + type entry struct{ + name string + path string + size uint64 + } + var games []entry + var total uint64 + + for _, tool := range s.CompatTools { + for _, game := range tool.Games { + if game.IsInstalled { + continue + } + + p := s.SearchCompatdataPath(game.ID) + if p == "" { + continue + } + + size, err := utils.DirSize(p) + exitOnError(err) + + games = append(games, entry{game.Name, p, size}) + } + } + + if len(games) > 0 { + fmt.Println() + } else { + fmt.Println("No unused compatdata directories found!") + os.Exit(0) + } + + for _, entry := range games { + total += entry.size + fmt.Printf("%10v %v\n", humanize.Bytes(entry.size), entry.name) + } + + fmt.Printf("\nTotal size: %v\n", humanize.Bytes(total)) + fmt.Println("WARNING: Backup save game data for games without Steam Cloud support!") + + if !yes { + isOK, err := utils.AskYesOrNo("Do you want to delete compatdata directories?") + exitOnError(err) + + if !isOK { + fmt.Println("Aborted") + return + } + } + + for _, entry := range games { + os.RemoveAll(entry.path) + } + + fmt.Println("Done") +} diff --git a/steam/LibraryConfig.go b/steam/LibraryConfig.go index dc544b4..18c3a25 100644 --- a/steam/LibraryConfig.go +++ b/steam/LibraryConfig.go @@ -10,6 +10,22 @@ type LibraryConfigVdf struct { Vdf } +func (vdf LibraryConfigVdf) GetLibraryPaths() []string { + var paths []string + + x := vdf.Root.FirstSubTree() + for { + paths = append(paths, x.FirstByName("path").String()) + + x = x.NextSubTree() + if x == nil { + break + } + } + + return paths +} + // GetLibraryPathByID returns library path for app id func (vdf LibraryConfigVdf) GetLibraryPathByID(id string) string { x := vdf.Root.FirstSubTree() diff --git a/steam/compatdata.go b/steam/compatdata.go new file mode 100644 index 0000000..25c6832 --- /dev/null +++ b/steam/compatdata.go @@ -0,0 +1,43 @@ +package steam + +import ( + "errors" + "os" + "path" +) + +// GetCompatdataPath returns compatdata path and game name for given game ID or +// name +func (s *Steam) GetCompatdataPath(idOrName string) (string, string, error) { + id, name, err := s.GetAppIDAndName(idOrName) + if err != nil { + return "", "", err + } + + p := s.LibraryConfig.GetLibraryPathByID(id) + if p == "" { + return "", "", errors.New("Game not installed") + } + + return path.Join(p, "steamapps", "compatdata", id), name, nil +} + +// SearchCompatdataPath searches for compatdata path for given game id in all +// library paths +func (s *Steam) SearchCompatdataPath(id string) string { + paths := s.LibraryConfig.GetLibraryPaths() + for _, p := range paths { + x := path.Join(p, "steamapps", "compatdata", id) + + info, err := os.Stat(x) + if err != nil { + continue + } + + if info.IsDir() { + return x + } + } + + return "" +} diff --git a/utils/dir.go b/utils/dir.go new file mode 100644 index 0000000..d9f0866 --- /dev/null +++ b/utils/dir.go @@ -0,0 +1,25 @@ +package utils + +import ( + "os" + "path/filepath" +) + +// DirSize retusn total size of directory +func DirSize(path string) (uint64, error) { + var size uint64 + + err := filepath.Walk(path, func(_ string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if !info.IsDir() { + size += uint64(info.Size()) + } + + return err + }) + + return size, err +}