esp-modman/manager.go

443 lines
9.6 KiB
Go
Raw Normal View History

2022-12-03 18:56:36 +01:00
package main
import (
2022-12-21 00:28:30 +01:00
"archive/zip"
2022-12-05 14:13:58 +01:00
"bufio"
2022-12-25 18:31:35 +01:00
"errors"
2022-12-03 18:56:36 +01:00
"fmt"
2022-12-05 14:13:58 +01:00
"io"
"io/fs"
2022-12-03 18:56:36 +01:00
"os"
"path/filepath"
2022-12-25 18:38:21 +01:00
"sauce.pizzawednes.day/kayomn/ini-grinder"
2022-12-21 00:28:30 +01:00
)
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
const appName = "modman"
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
var errGameNotFound = errors.New("game not found")
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
var gamesIniPath = filepath.Join(configDirPath(), "games.ini")
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
func CleanGameMods(gameName string) error {
var deployDirPath, pathError = gameDataPath(gameName)
2022-12-21 00:28:30 +01:00
2022-12-25 18:31:35 +01:00
if pathError != nil {
return pathError
2022-12-05 14:13:58 +01:00
}
2022-12-25 18:31:35 +01:00
// Loop over overwrite dir and move files back.
var cacheDirPath = filepath.Join(cacheDirPath(), gameName)
var stageDirPath = filepath.Join(cacheDirPath, "staged")
2022-12-21 00:28:30 +01:00
2022-12-25 18:31:35 +01:00
if walkError := filepath.WalkDir(stageDirPath, func(
path string, dirEntry fs.DirEntry, err error) error {
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if err != nil {
return err
2022-12-22 00:32:30 +01:00
}
2022-12-25 18:31:35 +01:00
if dirEntry.IsDir() {
return nil
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
var relativePath, relativeError = filepath.Rel(stageDirPath, path)
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if relativeError != nil {
return relativeError
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if removeError := os.Remove(filepath.Join(
deployDirPath, relativePath)); removeError != nil {
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if !(os.IsNotExist(removeError)) {
return removeError
2022-12-21 00:28:30 +01:00
}
2022-12-25 18:31:35 +01:00
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
return nil
}); walkError != nil {
if !(os.IsNotExist(walkError)) {
return walkError
2022-12-21 00:28:30 +01:00
}
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if removeError := os.RemoveAll(stageDirPath); removeError != nil {
return removeError
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
var overwriteDirPath = filepath.Join(cacheDirPath, "overwritten")
2022-12-21 00:28:30 +01:00
2022-12-25 18:31:35 +01:00
if walkError := filepath.WalkDir(overwriteDirPath, func(
path string, dirEntry fs.DirEntry, err error) error {
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if err != nil {
return err
2022-12-23 15:58:59 +01:00
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if dirEntry.IsDir() {
return nil
2022-12-21 00:28:30 +01:00
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
var relativePath, relativeError = filepath.Rel(overwriteDirPath, path)
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if relativeError != nil {
return relativeError
2022-12-21 00:28:30 +01:00
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if renameError := os.Rename(path, filepath.Join(
deployDirPath, relativePath)); renameError != nil {
2022-12-06 10:33:48 +01:00
2022-12-25 18:31:35 +01:00
return renameError
2022-12-05 14:13:58 +01:00
}
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
return nil
}); walkError != nil {
if !(os.IsNotExist(walkError)) {
return walkError
2022-12-23 15:58:59 +01:00
}
2022-12-05 14:13:58 +01:00
}
2022-12-25 18:31:35 +01:00
if removeError := os.RemoveAll(overwriteDirPath); removeError != nil {
return removeError
}
2022-12-21 00:28:30 +01:00
2022-12-25 18:31:35 +01:00
return nil
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
func CreateGame(gameName string, gameDataPath string) error {
var gameDataPaths = make(map[string]string)
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if fileInfo, statError := os.Stat(gameDataPath); statError == nil {
if !(fileInfo.IsDir()) {
return fmt.Errorf("game data path must be a valid directory")
2022-12-21 00:28:30 +01:00
}
2022-12-25 18:31:35 +01:00
} else {
2022-12-21 00:28:30 +01:00
return statError
2022-12-05 14:13:58 +01:00
}
2022-12-25 18:31:35 +01:00
if iniFile, openError := os.Open(gamesIniPath); openError == nil {
if (openError != nil) && !(os.IsNotExist(openError)) {
return openError
}
2022-12-21 00:28:30 +01:00
defer func() {
2022-12-25 18:31:35 +01:00
if closeError := iniFile.Close(); closeError != nil {
2022-12-21 00:28:30 +01:00
panic(closeError)
}
}()
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
var parser = ini.NewParser(iniFile)
2022-12-21 00:28:30 +01:00
for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() {
2022-12-25 18:31:35 +01:00
var section = parser.Section()
2022-12-21 00:28:30 +01:00
2022-12-25 18:31:35 +01:00
if section == gameName {
return fmt.Errorf("`%s` is already used by a game", section)
2022-12-21 00:28:30 +01:00
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
if entry.Key == "path" {
gameDataPaths[section] = entry.Value
}
2022-12-21 00:28:30 +01:00
}
2022-12-25 18:31:35 +01:00
if parseError := parser.Err(); parseError != nil {
return parseError
2022-12-21 00:28:30 +01:00
}
} else if !(os.IsNotExist(openError)) {
return openError
}
2022-12-03 19:27:53 +01:00
2022-12-25 18:31:35 +01:00
gameDataPaths[gameName] = gameDataPath
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
return saveGames(gameDataPaths)
2022-12-05 14:13:58 +01:00
}
2022-12-25 18:31:35 +01:00
func DeployGameMods(gameName string, modArchivePaths []string) error {
var deployDirPath, pathError = gameDataPath(gameName)
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
if pathError != nil {
return pathError
2022-12-22 00:32:30 +01:00
}
2022-12-25 18:31:35 +01:00
var overwrittenFilePaths = make(map[string]bool)
var cacheDirPath = filepath.Join(cacheDirPath(), gameName)
var overwriteDirPath = filepath.Join(cacheDirPath, "overwritten")
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
if walkError := filepath.WalkDir(overwriteDirPath, func(
path string, dirEntry fs.DirEntry, err error) error {
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
if err != nil {
return err
}
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
if !(dirEntry.IsDir()) {
var relativePath, relativeError = filepath.Rel(overwriteDirPath, path)
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
if relativeError != nil {
return relativeError
2022-12-22 00:32:30 +01:00
}
2022-12-25 18:31:35 +01:00
overwrittenFilePaths[relativePath] = true
}
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
return nil
}); walkError != nil {
if !(os.IsNotExist(walkError)) {
return walkError
2022-12-22 00:32:30 +01:00
}
}
2022-12-25 18:31:35 +01:00
var stageDirPath = filepath.Join(cacheDirPath, "staged")
2022-12-22 00:32:30 +01:00
2022-12-25 18:31:35 +01:00
for _, archivePath := range modArchivePaths {
var archiveExtension = filepath.Ext(archivePath)
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if len(archiveExtension) == 0 {
return fmt.Errorf("cannot infer archive format: `%s`", archivePath)
}
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
switch archiveExtension[1:] {
case "zip":
var zipReadCloser, openError = zip.OpenReader(archivePath)
2022-12-05 14:13:58 +01:00
2022-12-25 18:31:35 +01:00
if openError != nil {
return openError
}
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
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)
}
}()
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
for _, zipFile := range zipReadCloser.File {
var stagePath = filepath.Join(stageDirPath, zipFile.Name)
2022-12-25 18:31:35 +01:00
if dirError := os.MkdirAll(
filepath.Dir(stagePath), os.ModePerm); dirError != nil {
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
return dirError
}
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
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
}
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
default:
return fmt.Errorf("unrecognized archive format: `%s`", archivePath)
2022-12-23 15:58:59 +01:00
}
}
2022-12-25 18:31:35 +01:00
if walkError := filepath.WalkDir(stageDirPath, func(
path string, dirEntry fs.DirEntry, err error) error {
2022-12-25 18:31:35 +01:00
if dirEntry.IsDir() {
return nil
}
2022-12-25 18:31:35 +01:00
var relativePath, relativeError = filepath.Rel(stageDirPath, path)
2022-12-25 18:31:35 +01:00
if relativeError != nil {
return relativeError
}
2022-12-25 18:31:35 +01:00
var deployFilePath = filepath.Join(deployDirPath, relativePath)
if dirError := os.MkdirAll(filepath.Dir(deployFilePath), os.ModePerm); dirError != nil {
return dirError
}
2022-12-25 18:31:35 +01:00
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
}
}
2022-12-25 18:31:35 +01:00
if linkError := os.Link(path, deployFilePath); linkError != nil {
2022-12-25 19:57:28 +01:00
if !(os.IsNotExist(linkError)) {
return linkError
}
}
2022-12-25 18:31:35 +01:00
return nil
}); walkError != nil {
if !(os.IsNotExist(walkError)) {
return walkError
}
}
return nil
}
2022-12-25 18:31:35 +01:00
func RemoveGame(gameName string) error {
var gameDataPaths = make(map[string]string)
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
if iniFile, openError := os.Open(gamesIniPath); openError == nil {
if (openError != nil) && !(os.IsNotExist(openError)) {
return openError
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
defer func() {
if closeError := iniFile.Close(); closeError != nil {
panic(closeError)
}
}()
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
var parser = ini.NewParser(iniFile)
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() {
var section = parser.Section()
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
if (section != gameName) && (entry.Key == "path") {
gameDataPaths[section] = entry.Value
}
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
if parseError := parser.Err(); parseError != nil {
return parseError
}
} else if !(os.IsNotExist(openError)) {
return openError
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
return saveGames(gameDataPaths)
2022-12-21 00:28:30 +01:00
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
func cacheDirPath() string {
2022-12-23 15:58:59 +01:00
return fallbackPath(os.UserCacheDir, "cache")
}
2022-12-25 18:31:35 +01:00
func configDirPath() string {
2022-12-23 15:58:59 +01:00
return fallbackPath(os.UserConfigDir, "config")
}
func fallbackPath(getFalliblePath func() (string, error), fallbackSubdir string) string {
var path, pathError = getFalliblePath()
2022-12-03 18:56:36 +01:00
if pathError != nil {
2022-12-23 15:58:59 +01:00
// Fallback to homedir.
path, pathError = os.UserHomeDir()
if pathError != nil {
// User home dir should exist / be accessible.
panic(pathError)
}
2022-12-25 18:31:35 +01:00
return filepath.Join(path, appName, fallbackSubdir)
2022-12-03 18:56:36 +01:00
}
2022-12-25 18:31:35 +01:00
return filepath.Join(path, appName)
2022-12-23 15:58:59 +01:00
}
2022-12-03 18:56:36 +01:00
2022-12-25 18:31:35 +01:00
func gameDataPath(gameName string) (string, error) {
var gamesFile, openError = os.Open(gamesIniPath)
2022-12-25 18:31:35 +01:00
if (openError != nil) && !(os.IsNotExist(openError)) {
return "", openError
2022-12-03 18:56:36 +01:00
}
2022-12-25 18:31:35 +01:00
defer func() {
if closeError := gamesFile.Close(); closeError != nil {
panic(closeError)
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
}()
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
var parser = ini.NewParser(gamesFile)
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
for entry := parser.Parse(); !(parser.IsEnd()); entry = parser.Parse() {
if (parser.Section() == gameName) && (entry.Key == "path") {
return entry.Value, nil
2022-12-23 15:58:59 +01:00
}
}
2022-12-25 18:31:35 +01:00
if parseError := parser.Err(); parseError != nil {
return "", parseError
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
return "", errGameNotFound
}
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
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)
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
for name, dataPath := range gameDataPaths {
iniBuilder.Section(name)
iniBuilder.KeyValue("path", dataPath)
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
if flushError := iniWriter.Flush(); flushError != nil {
iniFile.Close()
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
return flushError
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
if syncError := iniFile.Sync(); syncError != nil {
iniFile.Close()
2022-12-23 15:58:59 +01:00
2022-12-25 18:31:35 +01:00
return syncError
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
if closeError := iniFile.Close(); closeError != nil {
return closeError
2022-12-23 15:58:59 +01:00
}
2022-12-25 18:31:35 +01:00
} else {
return createError
2022-12-03 18:56:36 +01:00
}
2022-12-25 18:31:35 +01:00
return nil
2022-12-03 18:56:36 +01:00
}