579 lines
14 KiB
Go
579 lines
14 KiB
Go
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 {
|
|
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 fileInfo, statError := os.Stat(deployPath); statError == nil {
|
|
var backupPath = filepath.Join(
|
|
cachePath(), game.Name, "overwritten", zipFile.Name)
|
|
|
|
if dirError := os.MkdirAll(filepath.Dir(backupPath), fileInfo.Mode()); dirError != nil {
|
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
|
return closeError
|
|
}
|
|
|
|
return dirError
|
|
}
|
|
|
|
if renameError := os.Rename(deployPath, backupPath); renameError != nil {
|
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
|
return closeError
|
|
}
|
|
|
|
return renameError
|
|
}
|
|
|
|
game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name)
|
|
} else if !(os.IsNotExist(statError)) {
|
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
|
return closeError
|
|
}
|
|
|
|
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.Key,
|
|
}
|
|
|
|
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(), "deployed.txt")
|
|
|
|
if len(game.DeployedFilePaths) == 0 {
|
|
return os.Truncate(listPath, 0)
|
|
}
|
|
|
|
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.Open(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
|
|
}
|