443 lines
9.6 KiB
Go
443 lines
9.6 KiB
Go
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 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
|
|
}
|