diff --git a/cmd/mccl/commands/uninstall_command.go b/cmd/mccl/commands/uninstall_command.go new file mode 100644 index 0000000..ceeb478 --- /dev/null +++ b/cmd/mccl/commands/uninstall_command.go @@ -0,0 +1,167 @@ +package commands + +import ( + "errors" + "fmt" + "mccl/internal/assets" + "mccl/internal/manifest" + "mccl/pkg/util" + "os" + "path" +) + +type UninstallCommand struct { + Id string + GameDir string +} + +func NewUninstallCommand(id, gameDir string) *UninstallCommand { + return &UninstallCommand{Id: id, GameDir: gameDir} +} + +func (uc *UninstallCommand) Run() error { + if uc.Id == "" { + return errors.New("an empty Minecraft version was provided") + } + if uc.GameDir == "" { + return errors.New("an empty path was provided") + } + return uc.uninstall_client() +} + +func (uc *UninstallCommand) uninstall_client() error { + var err error + + if uc.GameDir == "." { + execPath, _ := os.Executable() + uc.GameDir = path.Dir(execPath) + } + + if _, err := os.Stat(uc.GameDir); err != nil { + return os.ErrNotExist + } + + manifests := make(map[string]*manifest.Manifest) + + entries, err := os.ReadDir(path.Join(uc.GameDir, "versions")) + if err != nil { + return err + } + for _, entry := range entries { + if entry.IsDir() { + data, err := util.ReadFile(path.Join(uc.GameDir, "versions", entry.Name(), entry.Name()+".json")) + if err != nil { + return err + } + manifests[entry.Name()], err = manifest.New(data) + if err != nil { + return err + } + } + } + + for id, manifest := range manifests { + if id == uc.Id { + continue + } + + if manifest.InheritsFrom == uc.Id { + return fmt.Errorf("%s inherits %s. Uninstall %s first", id, uc.Id, id) + } + } + + assetIndexInUse := manifests[uc.Id].InheritsFrom != "" + logConfigInUse := manifests[uc.Id].InheritsFrom != "" + libsToDelete := make(map[string]string) + + for _, lib := range manifests[uc.Id].Libraries { + libsToDelete[lib.Name] = lib.Path() + } + + for id, manifest := range manifests { + if id == uc.Id { + continue + } + + if !assetIndexInUse && manifests[uc.Id].Assets == manifest.Assets { + assetIndexInUse = true + } + + if !logConfigInUse && manifests[uc.Id].Logging.Client.File.Id == manifest.Logging.Client.File.Id { + logConfigInUse = true + } + + for _, lib := range manifest.Libraries { + if name, ok := libsToDelete[lib.Name]; ok { + delete(libsToDelete, name) + } + } + } + + if !logConfigInUse { + dir := path.Join(uc.GameDir, "assets", "log_configs") + if err := os.Remove(path.Join(dir, manifests[uc.Id].Logging.Client.File.Id)); err != nil { + return fmt.Errorf("failed to remove assets/log_configs/%s: %s", manifests[uc.Id].Logging.Client.File.Id, err) + } + if d, err := os.ReadDir(dir); err == nil && len(d) == 0 { + if err := os.Remove(path.Join(dir, manifests[uc.Id].Logging.Client.File.Id)); err != nil { + return fmt.Errorf("failed to remove assets/log_configs directory: %s", err) + } + } + } + + if !assetIndexInUse { + dir := path.Join(uc.GameDir, "assets") + + assetFiles := make(map[string]*assets.Assets) + + for _, manifest := range manifests { + if _, ok := assetFiles[manifest.Assets]; !ok { + data, err := os.ReadFile(path.Join(dir, "indexes", manifest.Assets+".json")) + if err != nil { + return fmt.Errorf("failed to read an index %s file: %s", manifest.Assets, err) + } + assetFiles[manifest.Assets], err = assets.New(data) + if err != nil { + return fmt.Errorf("failed to load asset index %s: %s", manifest.Assets, err) + } + } + } + + toRemove := []string{} + + for name, obj := range assetFiles[manifests[uc.Id].Assets].Objects { + inUse := false + for id, idx := range assetFiles { + if id == manifests[uc.Id].Assets { + continue + } + if iobj, ok := idx.Objects[name]; ok && iobj.Hash == obj.Hash { + inUse = true + } + } + + if !inUse { + toRemove = append(toRemove, obj.Hash) + } + } + + for _, hash := range toRemove { + if err := os.Remove(path.Join(dir, "objects", hash[:2], hash)); err != nil { + return fmt.Errorf("failed to remove an object %s/%s: %s", hash[:2], hash, err) + } + } + + if err := os.Remove(path.Join(dir, "indexes", manifests[uc.Id].Assets+".json")); err != nil { + return fmt.Errorf("failed to remove %s: %s", path.Join("assets", "indexes", manifests[uc.Id].Assets+".json"), err) + } + } + + for _, path := range libsToDelete { + if err := os.Remove(path); err != nil { + return fmt.Errorf("failed to remove %s: %s", path, err) + } + } + + return nil +}