esp-modman/manager.go

473 lines
10 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 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
}