package main import ( "archive/zip" "bufio" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "sauce.pizzawednes.day/kayomn/ini-grinder" ) const appName = "modman" var errGameNotFound = errors.New("game not found") var gamesIniPath = filepath.Join(configDirPath(), "games.ini") func CleanGameMods(gameName string) error { var deployDirPath, pathError = gameDataPath(gameName) if pathError != nil { return pathError } // Loop over overwrite dir and move files back. var cacheDirPath = filepath.Join(cacheDirPath(), gameName) var stageDirPath = filepath.Join(cacheDirPath, "staged") if walkError := filepath.WalkDir(stageDirPath, func( path string, dirEntry fs.DirEntry, err error) error { if err != nil { return err } if dirEntry.IsDir() { return nil } var relativePath, relativeError = filepath.Rel(stageDirPath, path) if relativeError != nil { return relativeError } if removeError := os.Remove(filepath.Join( deployDirPath, relativePath)); removeError != nil { if !(os.IsNotExist(removeError)) { return removeError } } return nil }); walkError != nil { if !(os.IsNotExist(walkError)) { return walkError } } if removeError := os.RemoveAll(stageDirPath); removeError != nil { return removeError } var overwriteDirPath = filepath.Join(cacheDirPath, "overwritten") if walkError := filepath.WalkDir(overwriteDirPath, func( path string, dirEntry fs.DirEntry, err error) error { if err != nil { return err } if dirEntry.IsDir() { return nil } var relativePath, relativeError = filepath.Rel(overwriteDirPath, path) if relativeError != nil { return relativeError } if renameError := os.Rename(path, filepath.Join( deployDirPath, relativePath)); renameError != nil { return renameError } return nil }); walkError != nil { if !(os.IsNotExist(walkError)) { return walkError } } if removeError := os.RemoveAll(overwriteDirPath); removeError != nil { return removeError } return nil } func CreateGame(gameName string, gameDataPath string) error { var gameDataPaths = make(map[string]string) if fileInfo, statError := os.Stat(gameDataPath); statError == nil { if !(fileInfo.IsDir()) { return fmt.Errorf("game data path must be a valid directory") } } else { return statError } if iniFile, openError := os.Open(gamesIniPath); openError == nil { defer func() { if closeError := iniFile.Close(); closeError != nil { panic(closeError) } }() var parser = ini.NewParser(iniFile) for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { var section = parser.Section() if section == gameName { return fmt.Errorf("`%s` is already used by a game", section) } if entry.Key == "path" { gameDataPaths[section] = entry.Value } } if parseError := parser.Err(); parseError != nil { return parseError } } else if !(os.IsNotExist(openError)) { return openError } gameDataPaths[gameName] = gameDataPath return saveGames(gameDataPaths) } func DeployGameMods(gameName string, modArchivePaths []string) error { var deployDirPath, pathError = gameDataPath(gameName) if pathError != nil { return pathError } var overwrittenFilePaths = make(map[string]bool) var cacheDirPath = filepath.Join(cacheDirPath(), gameName) var overwriteDirPath = filepath.Join(cacheDirPath, "overwritten") if walkError := filepath.WalkDir(overwriteDirPath, func( path string, dirEntry fs.DirEntry, err error) error { if err != nil { return err } if !(dirEntry.IsDir()) { var relativePath, relativeError = filepath.Rel(overwriteDirPath, path) if relativeError != nil { return relativeError } overwrittenFilePaths[relativePath] = true } return nil }); walkError != nil { if !(os.IsNotExist(walkError)) { return walkError } } var stageDirPath = filepath.Join(cacheDirPath, "staged") for _, archivePath := range modArchivePaths { var archiveExtension = filepath.Ext(archivePath) if len(archiveExtension) == 0 { return fmt.Errorf("cannot infer archive format: `%s`", archivePath) } switch archiveExtension[1:] { case "zip": var zipReadCloser, openError = zip.OpenReader(archivePath) 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 stagePath = filepath.Join(stageDirPath, zipFile.Name) if dirError := os.MkdirAll( filepath.Dir(stagePath), os.ModePerm); dirError != nil { return dirError } if zipFile.FileInfo().IsDir() { // All work is done for creating a directory, rest is just for files. continue } var entryReadCloser, openError = zipFile.Open() if openError != nil { return openError } defer func() { if closeError := entryReadCloser.Close(); closeError != nil { // Zip entry read closer will not have any pending I/O operations nor is it // possible to have already been closed. panic(closeError) } }() if stagingFile, createError := os.Create(stagePath); createError == nil { if _, copyError := io.Copy(stagingFile, entryReadCloser); copyError != nil { stagingFile.Sync() stagingFile.Close() return copyError } if syncError := stagingFile.Sync(); syncError != nil { stagingFile.Close() return syncError } if closeError := stagingFile.Close(); closeError != nil { return closeError } } else { return createError } } default: return fmt.Errorf("unrecognized archive format: `%s`", archivePath) } } if walkError := filepath.WalkDir(stageDirPath, func( path string, dirEntry fs.DirEntry, err error) error { if dirEntry.IsDir() { return nil } var relativePath, relativeError = filepath.Rel(stageDirPath, path) if relativeError != nil { return relativeError } var deployFilePath = filepath.Join(deployDirPath, relativePath) if dirError := os.MkdirAll(filepath.Dir(deployFilePath), os.ModePerm); dirError != nil { return dirError } if isOverwrittenFilePath := overwrittenFilePaths[relativePath]; !(isOverwrittenFilePath) { var ovewriteFilePath = filepath.Join(overwriteDirPath, relativePath) if _, statError := os.Stat(ovewriteFilePath); statError == nil { if dirError := os.MkdirAll( filepath.Dir(ovewriteFilePath), os.ModePerm); dirError != nil { return dirError } if renameError := os.Rename(deployFilePath, ovewriteFilePath); renameError != nil { return renameError } } else if !(os.IsNotExist(statError)) { return statError } } if linkError := os.Link(path, deployFilePath); linkError != nil { if !(os.IsNotExist(linkError)) { return linkError } } return nil }); walkError != nil { if !(os.IsNotExist(walkError)) { return walkError } } return nil } func LoadGames() (map[string]string, error) { var gameDataPaths = make(map[string]string) if iniFile, openError := os.Open(gamesIniPath); openError == nil { defer func() { if closeError := iniFile.Close(); closeError != nil { panic(closeError) } }() var parser = ini.NewParser(iniFile) for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { var section = parser.Section() if entry.Key == "path" { gameDataPaths[section] = entry.Value } } if parseError := parser.Err(); parseError != nil { return gameDataPaths, parseError } } else if !(os.IsNotExist(openError)) { return gameDataPaths, openError } return gameDataPaths, nil } func RemoveGame(gameName string) error { if cleanError := CleanGameMods(gameName); cleanError != nil { return cleanError } var gameDataPaths = make(map[string]string) if iniFile, openError := os.Open(gamesIniPath); openError == nil { if (openError != nil) && !(os.IsNotExist(openError)) { return openError } defer func() { if closeError := iniFile.Close(); closeError != nil { panic(closeError) } }() var parser = ini.NewParser(iniFile) for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { var section = parser.Section() if (section != gameName) && (entry.Key == "path") { gameDataPaths[section] = entry.Value } } if parseError := parser.Err(); parseError != nil { return parseError } } else if !(os.IsNotExist(openError)) { return openError } return saveGames(gameDataPaths) } func cacheDirPath() string { return fallbackPath(os.UserCacheDir, "cache") } func configDirPath() 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, appName, fallbackSubdir) } return filepath.Join(path, appName) } func gameDataPath(gameName string) (string, error) { var gamesFile, openError = os.Open(gamesIniPath) if (openError != nil) && !(os.IsNotExist(openError)) { return "", openError } defer func() { if closeError := gamesFile.Close(); closeError != nil { panic(closeError) } }() var parser = ini.NewParser(gamesFile) for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() { if (parser.Section() == gameName) && (entry.Key == "path") { return entry.Value, nil } } if parseError := parser.Err(); parseError != nil { return "", parseError } return "", errGameNotFound } func saveGames(gameDataPaths map[string]string) error { if iniFile, createError := os.Create(gamesIniPath); createError == nil { var iniWriter = bufio.NewWriter(iniFile) var iniBuilder = ini.NewBuilder(iniWriter) for name, dataPath := range gameDataPaths { iniBuilder.Section(name) iniBuilder.KeyValue("path", dataPath) } if flushError := iniWriter.Flush(); flushError != nil { iniFile.Close() return flushError } if syncError := iniFile.Sync(); syncError != nil { iniFile.Close() return syncError } if closeError := iniFile.Close(); closeError != nil { return closeError } } else { return createError } return nil }