From d35eb6016d652ead50634551243e9cb1cd9b2cec Mon Sep 17 00:00:00 2001 From: kayomn Date: Fri, 23 Dec 2022 14:58:59 +0000 Subject: [PATCH] Complete code audit --- go.mod | 2 +- go.sum | 4 +- main.go | 145 ++++-------- manager.go | 680 ++++++++++++++++++++++++++--------------------------- 4 files changed, 385 insertions(+), 446 deletions(-) diff --git a/go.mod b/go.mod index 65e0a69..9afcf31 100644 --- a/go.mod +++ b/go.mod @@ -4,5 +4,5 @@ go 1.19 require ( gopkg.in/yaml.v3 v3.0.1 // indirect - sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa // indirect + sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418 // indirect ) diff --git a/go.sum b/go.sum index b93cb87..3ae7635 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa h1:Co1oVW0IMEIwk3tlQ2dJEFzNSd4r6tkPb2/6mxjnsOo= -sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa/go.mod h1:lniG+VCTpfcWAKKudVYLrS5NIpRx90H3mQklQNn+eK0= +sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418 h1:lAFE8yL+87+JGZROd2Wc3PG2BUWyiKe0xOWTNyVEiAU= +sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418/go.mod h1:lniG+VCTpfcWAKKudVYLrS5NIpRx90H3mQklQNn+eK0= diff --git a/main.go b/main.go index ce1855b..e25f852 100644 --- a/main.go +++ b/main.go @@ -32,32 +32,16 @@ var commands = []Command{ requiredArguments[0], requiredArguments[1]) } - var game, gameOpenError = OpenGame(providedArguments[0]) - - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) - } - }() - var archivePaths = providedArguments[1:] - if len(archivePaths) > 1 { - for _, archivePath := range archivePaths { - if installError := game.InstallMod(archivePath); installError != nil { - return "", installError - } + if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil { + if installError := game.InstallMods(archivePaths); installError != nil { + return "", installError } - - return "mods installed", nil } - if installError := game.InstallMod(archivePaths[0]); installError != nil { - return "", installError + if len(archivePaths) > 1 { + return "mods installed", nil } return "mod installed", nil @@ -99,23 +83,23 @@ var commands = []Command{ requiredArguments[0], requiredArguments[1]) } - var game, gameOpenError = OpenGame(providedArguments[0]) + var modNames = providedArguments[1:] - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) + if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil { + for _, name := range modNames { + if removeError := game.RemoveMod(name); removeError != nil { + return "", removeError + } } - }() - - if removeModsError := game.RemoveMods(providedArguments[1:]); removeModsError != nil { - return "", removeModsError + } else { + return "", openGameError } - return "removed mods", nil + if len(modNames) > 1 { + return "removed mods", nil + } + + return "removed mod", nil }, }, @@ -131,22 +115,12 @@ var commands = []Command{ requiredArguments[0], requiredArguments[1], requiredArguments[2]) } - var game, gameOpenError = OpenGame(providedArguments[0]) - - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) + if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil { + if renameError := game.RenameMod(providedArguments[1], providedArguments[2]); renameError != nil { + return "", renameError } - }() - - if renameError := game.RenameMod( - providedArguments[1], providedArguments[2]); renameError != nil { - - return "", renameError + } else { + return "", openGameError } return "renamed", nil @@ -165,29 +139,20 @@ var commands = []Command{ requiredArguments[0], requiredArguments[1]) } - var game, gameOpenError = OpenGame(providedArguments[0]) - - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) - } - }() - - var format = providedArguments[1] - var formatter, formatterExists = formatters[format] - - if !(formatterExists) { - return "", fmt.Errorf("unsupported format: `%s`", format) - } - var manifestBuilder = strings.Builder{} - if formatError := formatter(&manifestBuilder, game.Mods); formatError != nil { - return "", formatError + if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil { + var format = providedArguments[1] + + if formatter, exists := formatters[format]; exists { + if formatError := formatter(&manifestBuilder, game.Mods); formatError != nil { + return "", formatError + } + } else { + return "", fmt.Errorf("unsupported format: `%s`", format) + } + } else { + return "", openGameError } return manifestBuilder.String(), nil @@ -206,26 +171,12 @@ var commands = []Command{ requiredArguments[0], requiredArguments[1]) } - var game, gameOpenError = OpenGame(providedArguments[0]) - - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) - } - }() - - if cleanError := game.CleanDeployedMods(); cleanError != nil { - return "", cleanError - } - - for _, modName := range providedArguments[1:] { - if deployError := game.DeployMod(modName); deployError != nil { + if game, openGameError := LoadGame(providedArguments[0]); openGameError != nil { + if deployError := game.Deploy(providedArguments[1:]); deployError != nil { return "", deployError } + } else { + return "", openGameError } return "deployed", nil @@ -243,20 +194,12 @@ var commands = []Command{ return "", fmt.Errorf("expected %s", requiredArguments[0]) } - var game, gameOpenError = OpenGame(arguments[0]) - - if gameOpenError != nil { - return "", gameOpenError - } - - defer func() { - if closeError := game.Close(); closeError != nil { - panic(closeError) + if game, openGameError := LoadGame(arguments[0]); openGameError == nil { + if cleanError := game.Clean(); cleanError != nil { + return "", cleanError } - }() - - if cleanDeployedModsError := game.CleanDeployedMods(); cleanDeployedModsError != nil { - return "", cleanDeployedModsError + } else { + return "", openGameError } return "cleaned", nil diff --git a/manager.go b/manager.go index a245d12..03faa0e 100644 --- a/manager.go +++ b/manager.go @@ -13,29 +13,23 @@ import ( "sauce.pizzawednes.day/kayomn/ini-gusher" ) -func (game *Game) CleanDeployedMods() error { +func (game *Game) Clean() error { // Clean up currently deployed files first. for _, filePath := range game.DeployedFilePaths { - var gameFilePath = filepath.Join(game.Path, filePath) + if removeError := os.Remove(filepath.Join( + game.Path, filePath)); (removeError != nil) && (!(os.IsNotExist(removeError))) { - if removeError := os.Remove(gameFilePath); removeError != nil { - if !(os.IsNotExist(removeError)) { - return removeError - } + return removeError } } game.DeployedFilePaths = game.DeployedFilePaths[:0] + var overwriteDirPath = filepath.Join(cachePath(), game.Name, "overwritten") + // Then restore all files overwritten by previously deployment. for _, filePath := range game.OverwrittenFilePaths { - var backupDirPath, backupDirPathError = game.cachePath("overwritten") - - if backupDirPathError != nil { - return backupDirPathError - } - - if renameError := os.Rename(filepath.Join(backupDirPath, filePath), + if renameError := os.Rename(filepath.Join(overwriteDirPath, filePath), filepath.Join(game.Path, filePath)); renameError != nil { return renameError @@ -44,199 +38,119 @@ func (game *Game) CleanDeployedMods() error { game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0] - return nil + return game.saveDeployment() } -func (game *Game) Close() error { - // Write deployed files to disk. - var deployedListPath, deployedListPathError = game.cachePath("deployed.txt") +func (game *Game) Deploy(names []string) error { + for _, name := range names { + var mod, exists = game.Mods[name] - if deployedListPathError != nil { - return deployedListPathError - } - - var deployedListFile, deployedListCreateError = os.Create(deployedListPath) - - if deployedListCreateError != nil { - return deployedListCreateError - } - - defer func() { - if syncError := deployedListFile.Sync(); syncError != nil { - panic(syncError) + if !(exists) { + return fmt.Errorf("mod does not exist: %s", name) } - if closeError := deployedListFile.Close(); closeError != nil { - panic(closeError) - } - }() + var installPath = fmt.Sprintf("%s.%s", + filepath.Join(configPath(), game.Name, name), mod.Format) - var deployedListWriter = bufio.NewWriter(deployedListFile) + switch mod.Format { + case "zip": + var zipReadCloser, openError = zip.OpenReader(installPath) - defer func() { - if flushError := deployedListWriter.Flush(); flushError != nil { - panic(flushError) - } - }() - - for _, filePath := range game.DeployedFilePaths { - if _, writeError := deployedListWriter.WriteString(filePath); writeError != nil { - return writeError - } - - if writeError := deployedListWriter.WriteByte('\n'); writeError != nil { - return writeError - } - } - - // Read mod info from disk. - var modInfoPath, modInfoPathError = game.configPath("mods.ini") - - if modInfoPathError != nil { - return modInfoPathError - } - - var modInfoFile, modInfoCreateError = os.Create(modInfoPath) - - if modInfoCreateError != nil { - return modInfoCreateError - } - - defer func() { - if closeError := modInfoFile.Close(); closeError != nil { - panic(closeError) - } - }() - - var modInfoEntries = make([]ini.Entry, 0, len(game.Mods)*3) - - for name, mod := range game.Mods { - modInfoEntries = append(modInfoEntries, ini.Entry{ - Section: name, - Key: "source", - Value: mod.Source, - }) - - modInfoEntries = append(modInfoEntries, ini.Entry{ - Section: name, - Key: "format", - Value: mod.Format, - }) - - modInfoEntries = append(modInfoEntries, ini.Entry{ - Section: name, - Key: "version", - Value: mod.Version, - }) - } - - return ini.Write(modInfoFile, modInfoEntries) -} - -func (game *Game) DeployMod(name string) error { - var mod, exists = game.Mods[name] - - if !(exists) { - return fmt.Errorf("mod does not exist: %s", name) - } - - var archivePath, archivePathError = game.configPath(name) - - if archivePathError != nil { - return archivePathError - } - - switch mod.Format { - case "zip": - archivePath += ".zip" - - var zipReadCloser, zipReadCloserOpenError = zip.OpenReader(archivePath) - - if zipReadCloserOpenError != nil { - return zipReadCloserOpenError - } - - defer func() { - if closeError := zipReadCloser.Close(); closeError != nil { - panic(closeError) - } - }() - - for _, zipFile := range zipReadCloser.File { - var deployPath = filepath.Join(game.Path, zipFile.Name) - var fileMode = zipFile.Mode() - - if dirError := os.MkdirAll(filepath.Dir(deployPath), fileMode); dirError != nil { - return dirError - } - - if zipFile.FileInfo().IsDir() { - // All work is done for creating a directory, rest is just for files. - continue - } - - var file, openFileError = os.OpenFile( - deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) - - if openFileError != nil { - if !(os.IsExist(openFileError)) { - return openFileError - } - - var backupPath, backupPathError = game.cachePath("overwritten") // deployPath - - if backupPathError != nil { - return backupPathError - } - - backupPath = filepath.Join(backupPath, zipFile.Name) - - if dirError := os.MkdirAll(filepath.Dir(backupPath), 0755); dirError != nil { - return dirError - } - - if renameError := os.Rename(deployPath, backupPath); renameError != nil { - return renameError - } - - game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name) - - file, openFileError = os.OpenFile( - deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode) - - if openFileError != nil { - return openFileError - } - } - - var zipReadCloser, zipFileOpenError = zipFile.Open() - - if zipFileOpenError != nil { - return zipFileOpenError + if openError != nil { + return openError } defer func() { - if syncError := file.Sync(); syncError != nil { - panic(syncError) - } - - if closeError := file.Close(); closeError != nil { + if closeError := zipReadCloser.Close(); closeError != nil { + // Zip read closer will not have any pending I/O operations nor is it possible + // to have already been closed. panic(closeError) } }() - if _, copyError := io.Copy(file, zipReadCloser); copyError != nil { - return copyError + for _, zipFile := range zipReadCloser.File { + var deployPath = filepath.Join(game.Path, zipFile.Name) + var fileMode = zipFile.Mode() + + if dirError := os.MkdirAll(filepath.Dir(deployPath), fileMode); dirError != nil { + return dirError + } + + if zipFile.FileInfo().IsDir() { + // All work is done for creating a directory, rest is just for files. + continue + } + + // Backup up any pre-existing file before it is overwritten by Zip entry + // extraction. + if fileInfo, statError := os.Stat(deployPath); statError == nil { + var backupPath = filepath.Join( + cachePath(), game.Name, "overwritten", zipFile.Name) + + if dirError := os.MkdirAll(filepath.Dir(backupPath), fileInfo.Mode()); dirError != nil { + if closeError := zipReadCloser.Close(); closeError != nil { + return closeError + } + + return dirError + } + + if renameError := os.Rename(deployPath, backupPath); renameError != nil { + if closeError := zipReadCloser.Close(); closeError != nil { + return closeError + } + + return renameError + } + + game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name) + } else if !(os.IsNotExist(statError)) { + if closeError := zipReadCloser.Close(); closeError != nil { + return closeError + } + + return statError + } + + if file, createError := os.Create(deployPath); createError == nil { + var zipFile, ioError = zipFile.Open() + + if ioError == nil { + _, ioError = io.Copy(file, zipFile) + } + + if syncError := file.Sync(); (syncError != nil) && (ioError == nil) { + ioError = syncError + } + + if closeError := file.Close(); (closeError != nil) && (ioError == nil) { + ioError = closeError + } + + if closeError := zipFile.Close(); closeError != nil { + ioError = closeError + } + + if ioError != nil { + return ioError + } + } else { + if closeError := zipReadCloser.Close(); closeError != nil { + return closeError + } + + return createError + } + + game.DeployedFilePaths = append(game.DeployedFilePaths, zipFile.Name) } - game.DeployedFilePaths = append(game.DeployedFilePaths, zipFile.Name) + default: + return fmt.Errorf("unsupported mod format: %s", mod.Format) } - - default: - return fmt.Errorf("unsupported mod format: %s", mod.Format) } - return nil + return game.saveDeployment() } type Game struct { @@ -247,91 +161,93 @@ type Game struct { Path string } -func (game *Game) InstallMod(archivePath string) error { - var archiveBaseName = filepath.Base(archivePath) - var archiveExtension = filepath.Ext(archiveBaseName) +func (game *Game) InstallMods(archivePaths []string) error { + var installDirPath = filepath.Join(configPath(), game.Name) - if len(archiveExtension) == 0 { - return fmt.Errorf("unknown archive format: %s", archiveExtension) - } + for _, archivePath := range archivePaths { + var archiveName = filepath.Base(archivePath) + var archiveExtension = filepath.Ext(archiveName) - var name = strings.TrimSuffix(archiveBaseName, archiveExtension) - - if _, exists := game.Mods[name]; exists { - return fmt.Errorf("mod with name already exists: `%s`", name) - } - - // Copy archive into installation directory. - { - var installPath, installPathError = game.configPath(archiveBaseName) - - if installPathError != nil { - return installPathError + if len(archiveExtension) == 0 { + return fmt.Errorf("unknown archive format: %s", archiveExtension) } - var sourceFile, sourceOpenError = os.Open(archivePath) + var name = strings.TrimSuffix(archiveName, archiveExtension) - if sourceOpenError != nil { - return sourceOpenError + if _, exists := game.Mods[name]; exists { + return fmt.Errorf("mod with name already exists: `%s`", name) } - defer sourceFile.Close() + // Copy archive into installation directory. + if archiveFile, openError := os.Open(archivePath); openError == nil { + defer func() { + if closeError := archiveFile.Close(); closeError != nil { + // Archive file will not have any pending I/O operations nor is it possible to have + // already been closed. + panic(closeError) + } + }() - var targetFile, targetCreateError = os.Create(installPath) + var archiveFileInfo, statError = archiveFile.Stat() - if targetCreateError != nil { - return targetCreateError + if statError != nil { + return statError + } + + if dirError := os.MkdirAll(installDirPath, archiveFileInfo.Mode()); dirError != nil { + return dirError + } + + if installFile, createError := os.Create(filepath.Join(installDirPath, archiveName)); createError == nil { + var _, copyError = io.Copy(installFile, archiveFile) + var syncError = installFile.Sync() + var closeError = installFile.Close() + + if (copyError != nil) || (syncError != nil) { + return fmt.Errorf("failed to install mod") + } + + if closeError != nil { + return closeError + } + } else { + return createError + } + } else { + return openError } - defer targetFile.Close() - - if _, copyError := io.Copy(targetFile, sourceFile); copyError != nil { - return copyError + game.Mods[name] = Mod{ + Format: archiveExtension[1:], + Source: archivePath, + Version: "", } } - game.Mods[name] = Mod{ - Format: archiveExtension[1:], - Source: archivePath, - Version: "", - } - - return nil + return game.saveMods() } func (game *Game) Load() error { // Read deployed files from disk. - var deployedListPath, deployedListPathError = game.cachePath("deployed.txt") - - if deployedListPathError != nil { - return deployedListPathError - } + var deployedListPath = filepath.Join(cachePath(), game.Name, "deployed.txt") if file, openError := os.Open(deployedListPath); openError == nil { - defer func() { - if closeError := file.Close(); closeError != nil { - panic(closeError) - } - }() - - var scanner = bufio.NewScanner(file) - - for scanner.Scan() { + for scanner := bufio.NewScanner(file); scanner.Scan(); { game.DeployedFilePaths = append(game.DeployedFilePaths, scanner.Text()) } + + if closeError := file.Close(); closeError != nil { + return closeError + } } else if !(os.IsNotExist(openError)) { return openError } - // Read overwritten files from disk. - var overwriteDirPath, overwriteDirPathError = game.cachePath("overwritten") + // Read overwritten game files from disk. + var overwrittenFilesDirPath = filepath.Join(cachePath(), game.Name, "overwritten") - if overwriteDirPathError != nil { - return overwriteDirPathError - } - - if _, statError := os.Stat(overwriteDirPath); statError == nil { - if walkError := filepath.WalkDir(overwriteDirPath, func( + if _, statError := os.Stat(overwrittenFilesDirPath); statError == nil { + if walkError := filepath.WalkDir(overwrittenFilesDirPath, func( path string, dirEntry fs.DirEntry, walkError error) error { if walkError != nil { @@ -351,15 +267,11 @@ func (game *Game) Load() error { } // Read mod info from disk. - var modInfoPath, modInfoPathError = game.configPath("mods.ini") - - if modInfoPathError != nil { - return modInfoPathError - } - - if file, openError := os.Open(modInfoPath); openError == nil { + if file, openError := os.Open(filepath.Join(configPath(), game.Name, "mods.ini")); openError == nil { defer func() { if closeError := file.Close(); closeError != nil { + // File will not have any pending I/O operations nor is it possible to have already + // been closed. panic(closeError) } }() @@ -399,7 +311,7 @@ type Mod struct { Version string } -func OpenGame(name string) (Game, error) { +func LoadGame(name string) (Game, error) { var configPath, configPathError = os.UserConfigDir() if configPathError != nil { @@ -425,7 +337,7 @@ func OpenGame(name string) (Game, error) { OverwrittenFilePaths: make([]string, 0, 512), DeployedFilePaths: make([]string, 0, 512), Mods: make(map[string]Mod), - Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data", + Path: entry.Key, } if loadError := game.Load(); loadError != nil { @@ -439,84 +351,93 @@ func OpenGame(name string) (Game, error) { return Game{}, fmt.Errorf("game not registered: %s", name) } -func (game *Game) RemoveMods(names []string) error { - for _, name := range names { - if _, exists := game.Mods[name]; !(exists) { - return fmt.Errorf("unknown mod: `%s`", name) - } - - var path, pathError = game.configPath(name) - - if pathError != nil { - return pathError - } - - if removeError := os.RemoveAll(path); removeError != nil { - return removeError - } - - delete(game.Mods, name) +func (game *Game) RemoveMod(name string) error { + if _, exists := game.Mods[name]; !(exists) { + return fmt.Errorf("unknown mod: `%s`", name) } - return nil + if removeError := os.RemoveAll(filepath.Join( + configPath(), game.Name, name)); removeError != nil { + + return removeError + } + + delete(game.Mods, name) + + return game.saveMods() } func RegisterGame(name string, dataPath string) error { - var configPath, configPathError = os.UserConfigDir() + var gamesPath = filepath.Join(configPath(), "games.ini") + var gameNamePaths = make(map[string]string) - if configPathError != nil { - return configPathError + if file, openError := os.Open(gamesPath); openError == nil { + defer func() { + if closeError := file.Close(); closeError != nil { + // No way for this to fail. + panic(closeError) + } + }() + + var parser = ini.NewParser(file) + + for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { + if entry.Key == "path" { + gameNamePaths[entry.Section] = entry.Value + } + } + + if parserError := parser.Err(); parserError != nil { + return parserError + } + } else if !(os.IsNotExist(openError)) { + return openError } - var gamesPath = filepath.Join(configPath, "modman", "games.ini") + if file, updateError := os.Create(gamesPath); updateError == nil { + var writer = bufio.NewWriter(file) + var builder = ini.NewBuilder(writer) - var gamesEntries = []ini.Entry{{ - Section: name, - Key: "path", - Value: dataPath, - }} + for name, path := range gameNamePaths { + if buildError := builder.Section(name); buildError != nil { + updateError = buildError - if gamesFile, gamesFileError := os.Open(gamesPath); gamesFileError == nil { - var gamesParser = ini.NewParser(gamesFile) + break + } - for entry := gamesParser.Parse(); !(gamesParser.IsEnd()); entry = gamesParser.Parse() { - gamesEntries = append(gamesEntries, entry) + if buildError := builder.KeyValue("path", path); buildError != nil { + updateError = buildError + + break + } } - if err := gamesFile.Close(); err != nil { - return err + if writeError := writer.Flush(); (writeError != nil) && (updateError == nil) { + updateError = writeError } - if err := gamesParser.Err(); err != nil { - return err - } - } else if !(os.IsNotExist(gamesFileError)) { - return gamesFileError - } - - if gamesFile, gamesFileError := os.Create(gamesPath); gamesFileError == nil { - var writeError = ini.Write(gamesFile, gamesEntries) - - if err := gamesFile.Sync(); err != nil { - return err + if syncError := file.Sync(); (syncError != nil) && (updateError == nil) { + updateError = syncError } - if err := gamesFile.Close(); err != nil { - return err + if closeError := file.Close(); (closeError != nil) && (updateError == nil) { + updateError = closeError } - if writeError != nil { - return writeError + if updateError != nil { + return updateError } } else { - return gamesFileError + return updateError } return nil } func (game *Game) RenameMod(modName string, newName string) error { - if _, exists := game.Mods[modName]; !(exists) { + var mod, exists = game.Mods[modName] + + if !(exists) { return fmt.Errorf("no mod with that name exists") } @@ -524,59 +445,134 @@ func (game *Game) RenameMod(modName string, newName string) error { return fmt.Errorf("a mod with the new name already exists") } - var modPath, modPathError = game.configPath(modName) + var modsDirPath = filepath.Join(configPath(), game.Name) - if modPathError != nil { - return modPathError - } + if renameError := os.Rename(filepath.Join(modsDirPath, modName), + filepath.Join(modsDirPath, newName)); renameError != nil { - var newPath, newPathError = game.configPath(modName) - - if newPathError != nil { - return newPathError - } - - if renameError := os.Rename(modPath, newPath); renameError != nil { return renameError } - var mod = game.Mods[modName] - game.Mods[newName] = mod delete(game.Mods, modName) + return game.saveMods() +} + +func cachePath() string { + return fallbackPath(os.UserCacheDir, "cache") +} + +func configPath() string { + return fallbackPath(os.UserConfigDir, "config") +} + +func fallbackPath(getFalliblePath func() (string, error), fallbackSubdir string) string { + var path, pathError = getFalliblePath() + + if pathError != nil { + // Fallback to homedir. + path, pathError = os.UserHomeDir() + + if pathError != nil { + // User home dir should exist / be accessible. + panic(pathError) + } + + return filepath.Join(path, "modman", fallbackSubdir) + } + + return filepath.Join(path, "modman") +} + +func (game *Game) saveDeployment() error { + var listPath = filepath.Join(cachePath(), "deployed.txt") + + if len(game.DeployedFilePaths) == 0 { + return os.Truncate(listPath, 0) + } + + if file, updateError := os.Create(listPath); updateError == nil { + var writer = bufio.NewWriter(file) + + for _, filePath := range game.DeployedFilePaths { + if _, printError := fmt.Fprintln(writer, filePath); printError != nil { + updateError = printError + + break + } + } + + if flushError := writer.Flush(); flushError != nil && updateError == nil { + updateError = flushError + } + + if syncError := file.Sync(); syncError != nil && updateError == nil { + updateError = syncError + } + + if closeError := file.Close(); closeError != nil && updateError == nil { + updateError = closeError + } + + if updateError != nil { + return updateError + } + } else { + return updateError + } + return nil } -func (game *Game) cachePath(path string) (string, error) { - var dirPath, pathError = os.UserCacheDir() +func (game *Game) saveMods() error { + var file, updateError = os.Open(filepath.Join(configPath(), game.Name, "mods.ini")) - if pathError != nil { - return "", pathError + if updateError != nil { + return updateError } - dirPath = filepath.Join(dirPath, "modman", game.Name) + var writer = bufio.NewWriter(file) + var builder = ini.NewBuilder(writer) - if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil { - return "", mkdirError + for name, mod := range game.Mods { + if buildError := builder.Section(name); buildError != nil { + updateError = buildError + + break + } + + if buildError := builder.KeyValue("format", mod.Format); buildError != nil { + updateError = buildError + + break + } + + if buildError := builder.KeyValue("source", mod.Source); buildError != nil { + updateError = buildError + + break + } + + if buildError := builder.KeyValue("version", mod.Version); buildError != nil { + updateError = buildError + + break + } } - return filepath.Join(dirPath, path), nil -} - -func (game *Game) configPath(path string) (string, error) { - var dirPath, pathError = os.UserConfigDir() - - if pathError != nil { - return "", pathError - } - - dirPath = filepath.Join(dirPath, "modman", game.Name) - - if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil { - return "", mkdirError - } - - return filepath.Join(dirPath, path), nil + if writeError := writer.Flush(); (writeError != nil) && (updateError == nil) { + updateError = writeError + } + + if syncError := file.Sync(); (syncError != nil) && (updateError == nil) { + updateError = syncError + } + + if closeError := file.Close(); (closeError != nil) && (updateError == nil) { + updateError = closeError + } + + return updateError }