package main import ( "archive/zip" "bufio" "fmt" "io" "io/fs" "os" "path/filepath" "strings" "sauce.pizzawednes.day/kayomn/ini-gusher" ) func (game *Game) Clean() error { // Clean up currently deployed files first. for _, filePath := range game.DeployedFilePaths { if removeError := os.Remove(filepath.Join( game.Path, filePath)); (removeError != nil) && (!(os.IsNotExist(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 { if renameError := os.Rename(filepath.Join(overwriteDirPath, filePath), filepath.Join(game.Path, filePath)); renameError != nil { return renameError } } game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0] return game.saveDeployment() } func (game *Game) Deploy(names []string) error { if cleanError := game.Clean(); cleanError != nil { return cleanError } for _, name := range names { var mod, exists = game.Mods[name] if !(exists) { return fmt.Errorf("mod does not exist: %s", name) } var installPath = fmt.Sprintf("%s.%s", filepath.Join(configPath(), game.Name, name), mod.Format) switch mod.Format { case "zip": var zipReadCloser, openError = zip.OpenReader(installPath) if openError != nil { return openError } defer func() { 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) } }() 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 _, statError := os.Stat(deployPath); statError == nil { var backupPath = filepath.Join( cachePath(), game.Name, "overwritten", zipFile.Name) if dirError := os.MkdirAll(filepath.Dir(backupPath), os.ModePerm); dirError != nil { return dirError } if renameError := os.Rename(deployPath, backupPath); renameError != nil { return renameError } game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name) } else if !(os.IsNotExist(statError)) { 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) } default: return fmt.Errorf("unsupported mod format: %s", mod.Format) } } return game.saveDeployment() } type Game struct { Name string DeployedFilePaths []string OverwrittenFilePaths []string Mods map[string]Mod Path string } func (game *Game) InstallMods(archivePaths []string) error { var installDirPath = filepath.Join(configPath(), game.Name) for _, archivePath := range archivePaths { var archiveName = filepath.Base(archivePath) var archiveExtension = filepath.Ext(archiveName) if len(archiveExtension) == 0 { return fmt.Errorf("unknown archive format: %s", archiveExtension) } var name = strings.TrimSuffix(archiveName, archiveExtension) if _, exists := game.Mods[name]; exists { return fmt.Errorf("mod with name already exists: `%s`", name) } // 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 archiveFileInfo, statError = archiveFile.Stat() 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 } game.Mods[name] = Mod{ Format: archiveExtension[1:], Source: archivePath, Version: "", } } return game.saveMods() } func (game *Game) Load() error { // Read deployed files from disk. var deployedListPath = filepath.Join(cachePath(), game.Name, "deployed.txt") if file, openError := os.Open(deployedListPath); openError == nil { 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 game files from disk. var overwrittenFilesDirPath = filepath.Join(cachePath(), game.Name, "overwritten") if _, statError := os.Stat(overwrittenFilesDirPath); statError == nil { if walkError := filepath.WalkDir(overwrittenFilesDirPath, func( path string, dirEntry fs.DirEntry, walkError error) error { if walkError != nil { return walkError } if !(dirEntry.IsDir()) { game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, path) } return nil }); walkError != nil { return walkError } } else if !(os.IsNotExist(statError)) { return statError } // Read mod info from disk. 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) } }() var parser = ini.NewParser(file) for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { var mod = game.Mods[entry.Section] switch entry.Key { case "format": mod.Format = entry.Value case "source": mod.Source = entry.Value case "version": mod.Version = entry.Value } game.Mods[entry.Section] = mod } if parserError := parser.Err(); parserError != nil { return parserError } } else if !(os.IsNotExist(openError)) { return openError } return nil } type Mod struct { Format string Source string Version string } func LoadGame(name string) (Game, error) { var configPath, configPathError = os.UserConfigDir() if configPathError != nil { return Game{}, configPathError } var gamesFile, gamesOpenError = os.Open(filepath.Join(configPath, "modman", "games.ini")) if gamesOpenError != nil { if os.IsNotExist(gamesOpenError) { return Game{}, fmt.Errorf("no games registered") } return Game{}, gamesOpenError } var gamesParser = ini.NewParser(gamesFile) for entry := gamesParser.Parse(); !(gamesParser.IsEnd()); entry = gamesParser.Parse() { if (entry.Key == "path") && (entry.Section == name) { var game = Game{ Name: name, OverwrittenFilePaths: make([]string, 0, 512), DeployedFilePaths: make([]string, 0, 512), Mods: make(map[string]Mod), Path: entry.Value, } if loadError := game.Load(); loadError != nil { return Game{}, loadError } return game, nil } } return Game{}, fmt.Errorf("game not registered: %s", name) } func (game *Game) RemoveMod(name string) error { if _, exists := game.Mods[name]; !(exists) { return fmt.Errorf("unknown mod: `%s`", name) } 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 gamesPath = filepath.Join(configPath(), "games.ini") var gameNamePaths = make(map[string]string) 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 } if file, updateError := os.Create(gamesPath); updateError == nil { var writer = bufio.NewWriter(file) var builder = ini.NewBuilder(writer) for name, path := range gameNamePaths { if buildError := builder.Section(name); buildError != nil { updateError = buildError break } if buildError := builder.KeyValue("path", path); buildError != nil { updateError = buildError break } } 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 } if updateError != nil { return updateError } } else { return updateError } return nil } func (game *Game) RenameMod(modName string, newName string) error { var mod, exists = game.Mods[modName] if !(exists) { return fmt.Errorf("no mod with that name exists") } if _, nameTaken := game.Mods[newName]; nameTaken { return fmt.Errorf("a mod with the new name already exists") } var modsDirPath = filepath.Join(configPath(), game.Name) if renameError := os.Rename(filepath.Join(modsDirPath, modName), filepath.Join(modsDirPath, newName)); renameError != nil { return renameError } 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(), game.Name, "deployed.txt") if len(game.DeployedFilePaths) == 0 { var truncateError = os.Truncate(listPath, 0) if !(os.IsNotExist(truncateError)) { return truncateError } } if dirError := os.MkdirAll(filepath.Dir(listPath), os.ModePerm); dirError != nil { return dirError } 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) saveMods() error { var file, updateError = os.Create(filepath.Join(configPath(), game.Name, "mods.ini")) if updateError != nil { return updateError } var writer = bufio.NewWriter(file) var builder = ini.NewBuilder(writer) 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 } } 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 }