package main import ( "archive/zip" "encoding/csv" "fmt" "io" "os" "path/filepath" "strings" ) type App struct { ConfigDirPath string } type Game struct { ID string Mods map[string]Mod HasUpdated bool } func (game *Game) InstallMods(archivePaths []string) ([]string, error) { var processed = 0 for i := range archivePaths { var archivePath = archivePaths[i] var suffixIndex = strings.LastIndex(archivePath, ".") if suffixIndex < 0 { return nil, fmt.Errorf("cannot determine file type of `%s`", archivePath) } var extension = archivePath[suffixIndex+1:] var extractor = extractors[extension] if extractor == nil { return nil, fmt.Errorf("unsupported file type `%s`", extension) } var gamePath, pathError = game.Path() if pathError != nil { return nil, pathError } var modName = filepath.Base(archivePath[:suffixIndex]) if extractError := extractor(archivePath, filepath.Join( gamePath, "mods", modName)); extractError != nil { return nil, extractError } game.Mods[modName] = Mod{ IsEnabled: false, } game.HasUpdated = true processed += 1 } return archivePaths[0:processed], nil } type Mod struct { IsEnabled bool } func WithGame(gameName string, action func(*Game) error) error { var supportedGames = []string{"fallout4", "falloutnv", "skyrim"} for i := range supportedGames { var supportedGame = supportedGames[i] if gameName == supportedGame { var game = Game{ ID: supportedGame, Mods: make(map[string]Mod), HasUpdated: false, } var gamePath, pathError = game.Path() if pathError != nil { return pathError } var manifestPath = filepath.Join(gamePath, "mods.csv") // Load manifest from disk. { var manifestFile, openError = os.Open(manifestPath) if openError == nil { defer manifestFile.Close() var manifestReader = csv.NewReader(manifestFile) var recordValues, recordError = manifestReader.Read() for recordValues != nil { if recordError != nil { return recordError } if len(recordValues) < 2 { return fmt.Errorf("could not read mod manifest data - may be corrupt") } var status = recordValues[0] var name = recordValues[1] game.Mods[name] = Mod{ IsEnabled: status == "*", } recordValues, recordError = manifestReader.Read() } } } if actionError := action(&game); actionError != nil { return actionError } // Save manifest back to disk. if game.HasUpdated { var manifestFile, openError = os.OpenFile(manifestPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) if openError != nil { return openError } defer manifestFile.Close() var manifestWriter = csv.NewWriter(manifestFile) for name, mod := range game.Mods { var status = "" if mod.IsEnabled { status = "*" } manifestWriter.Write([]string{status, name}) } manifestWriter.Flush() } return nil } } return fmt.Errorf("%s: game not supported", gameName) } func (game Game) Path() (string, error) { var configDirPath, configError = os.UserConfigDir() if configError != nil { return "", configError } return filepath.Join(configDirPath, "modman", game.ID), nil } func (game *Game) RemoveMods(modNames []string) ([]string, error) { var gamePath, pathError = game.Path() if pathError != nil { return nil, pathError } var processed = 0 for i := range modNames { var modName = modNames[i] if removeError := os.RemoveAll(filepath.Join(gamePath, modName)); removeError != nil { return nil, removeError } delete(game.Mods, modName) game.HasUpdated = true processed += 1 } return modNames[0:processed], nil } func (game *Game) RenameMod(modName string, newName string) error { var gamePath, pathError = game.Path() if pathError != nil { return pathError } if _, exists := game.Mods[modName]; !(exists) { return fmt.Errorf("no mod with that name exists") } if _, exists := game.Mods[newName]; exists { return fmt.Errorf("a mod with the new name already exists") } if renameError := os.Rename(filepath.Join(gamePath, modName), filepath.Join(gamePath, newName)); renameError != nil { return renameError } game.Mods[newName] = game.Mods[modName] delete(game.Mods, modName) game.HasUpdated = true return nil } var extractors = map[string]func(string, string) error{ "zip": func(archivePath string, destinationPath string) error { var zipReader, openReaderError = zip.OpenReader(archivePath) if openReaderError != nil { return openReaderError } defer func() { if closeError := zipReader.Close(); closeError != nil { panic(closeError.Error()) } }() if mkdirError := os.MkdirAll(destinationPath, 0755); mkdirError != nil { return mkdirError } for i := range zipReader.File { var file = zipReader.File[i] var fileReader, fileOpenError = file.Open() if fileOpenError != nil { return fileOpenError } defer func() { if closeError := fileReader.Close(); closeError != nil { panic(closeError.Error()) } }() var path = filepath.Join(destinationPath, file.Name) if file.FileInfo().IsDir() { if mkdirError := os.MkdirAll(path, file.Mode()); mkdirError != nil { return mkdirError } } else { if mkdirError := os.MkdirAll(filepath.Dir(path), file.Mode()); mkdirError != nil { return mkdirError } var extractedFile, fileOpenError = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.Mode()) if fileOpenError != nil { return fileOpenError } defer func() { if fileOpenError := extractedFile.Close(); fileOpenError != nil { panic(fileOpenError) } }() var _, copyError = io.Copy(extractedFile, fileReader) if copyError != nil { return copyError } } } return nil }, }