Complete code audit
This commit is contained in:
parent
cde4b75c5d
commit
d35eb6016d
2
go.mod
2
go.mod
|
@ -4,5 +4,5 @@ go 1.19
|
||||||
|
|
||||||
require (
|
require (
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa // indirect
|
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418 // indirect
|
||||||
)
|
)
|
||||||
|
|
4
go.sum
4
go.sum
|
@ -1,5 +1,5 @@
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa h1:Co1oVW0IMEIwk3tlQ2dJEFzNSd4r6tkPb2/6mxjnsOo=
|
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418 h1:lAFE8yL+87+JGZROd2Wc3PG2BUWyiKe0xOWTNyVEiAU=
|
||||||
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221220232638-8a897dbd24aa/go.mod h1:lniG+VCTpfcWAKKudVYLrS5NIpRx90H3mQklQNn+eK0=
|
sauce.pizzawednes.day/kayomn/ini-gusher v0.0.0-20221223145344-d68ddf116418/go.mod h1:lniG+VCTpfcWAKKudVYLrS5NIpRx90H3mQklQNn+eK0=
|
||||||
|
|
143
main.go
143
main.go
|
@ -32,34 +32,18 @@ var commands = []Command{
|
||||||
requiredArguments[0], requiredArguments[1])
|
requiredArguments[0], requiredArguments[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(providedArguments[0])
|
|
||||||
|
|
||||||
if gameOpenError != nil {
|
|
||||||
return "", gameOpenError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if closeError := game.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var archivePaths = providedArguments[1:]
|
var archivePaths = providedArguments[1:]
|
||||||
|
|
||||||
|
if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil {
|
||||||
|
if installError := game.InstallMods(archivePaths); installError != nil {
|
||||||
|
return "", installError
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if len(archivePaths) > 1 {
|
if len(archivePaths) > 1 {
|
||||||
for _, archivePath := range archivePaths {
|
|
||||||
if installError := game.InstallMod(archivePath); installError != nil {
|
|
||||||
return "", installError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "mods installed", nil
|
return "mods installed", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if installError := game.InstallMod(archivePaths[0]); installError != nil {
|
|
||||||
return "", installError
|
|
||||||
}
|
|
||||||
|
|
||||||
return "mod installed", nil
|
return "mod installed", nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -99,23 +83,23 @@ var commands = []Command{
|
||||||
requiredArguments[0], requiredArguments[1])
|
requiredArguments[0], requiredArguments[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(providedArguments[0])
|
var modNames = providedArguments[1:]
|
||||||
|
|
||||||
if gameOpenError != nil {
|
if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil {
|
||||||
return "", gameOpenError
|
for _, name := range modNames {
|
||||||
}
|
if removeError := game.RemoveMod(name); removeError != nil {
|
||||||
|
return "", removeError
|
||||||
defer func() {
|
}
|
||||||
if closeError := game.Close(); closeError != nil {
|
}
|
||||||
panic(closeError)
|
} else {
|
||||||
}
|
return "", openGameError
|
||||||
}()
|
|
||||||
|
|
||||||
if removeModsError := game.RemoveMods(providedArguments[1:]); removeModsError != nil {
|
|
||||||
return "", removeModsError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(modNames) > 1 {
|
||||||
return "removed mods", nil
|
return "removed mods", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return "removed mod", nil
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -131,23 +115,13 @@ var commands = []Command{
|
||||||
requiredArguments[0], requiredArguments[1], requiredArguments[2])
|
requiredArguments[0], requiredArguments[1], requiredArguments[2])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(providedArguments[0])
|
if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil {
|
||||||
|
if renameError := game.RenameMod(providedArguments[1], providedArguments[2]); renameError != nil {
|
||||||
if gameOpenError != nil {
|
|
||||||
return "", gameOpenError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if closeError := game.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if renameError := game.RenameMod(
|
|
||||||
providedArguments[1], providedArguments[2]); renameError != nil {
|
|
||||||
|
|
||||||
return "", renameError
|
return "", renameError
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "", openGameError
|
||||||
|
}
|
||||||
|
|
||||||
return "renamed", nil
|
return "renamed", nil
|
||||||
},
|
},
|
||||||
|
@ -165,30 +139,21 @@ var commands = []Command{
|
||||||
requiredArguments[0], requiredArguments[1])
|
requiredArguments[0], requiredArguments[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(providedArguments[0])
|
|
||||||
|
|
||||||
if gameOpenError != nil {
|
|
||||||
return "", gameOpenError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if closeError := game.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var format = providedArguments[1]
|
|
||||||
var formatter, formatterExists = formatters[format]
|
|
||||||
|
|
||||||
if !(formatterExists) {
|
|
||||||
return "", fmt.Errorf("unsupported format: `%s`", format)
|
|
||||||
}
|
|
||||||
|
|
||||||
var manifestBuilder = strings.Builder{}
|
var manifestBuilder = strings.Builder{}
|
||||||
|
|
||||||
|
if game, openGameError := LoadGame(providedArguments[0]); openGameError == nil {
|
||||||
|
var format = providedArguments[1]
|
||||||
|
|
||||||
|
if formatter, exists := formatters[format]; exists {
|
||||||
if formatError := formatter(&manifestBuilder, game.Mods); formatError != nil {
|
if formatError := formatter(&manifestBuilder, game.Mods); formatError != nil {
|
||||||
return "", formatError
|
return "", formatError
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "", fmt.Errorf("unsupported format: `%s`", format)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return "", openGameError
|
||||||
|
}
|
||||||
|
|
||||||
return manifestBuilder.String(), nil
|
return manifestBuilder.String(), nil
|
||||||
},
|
},
|
||||||
|
@ -206,26 +171,12 @@ var commands = []Command{
|
||||||
requiredArguments[0], requiredArguments[1])
|
requiredArguments[0], requiredArguments[1])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(providedArguments[0])
|
if game, openGameError := LoadGame(providedArguments[0]); openGameError != nil {
|
||||||
|
if deployError := game.Deploy(providedArguments[1:]); deployError != nil {
|
||||||
if gameOpenError != nil {
|
|
||||||
return "", gameOpenError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if closeError := game.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if cleanError := game.CleanDeployedMods(); cleanError != nil {
|
|
||||||
return "", cleanError
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, modName := range providedArguments[1:] {
|
|
||||||
if deployError := game.DeployMod(modName); deployError != nil {
|
|
||||||
return "", deployError
|
return "", deployError
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "", openGameError
|
||||||
}
|
}
|
||||||
|
|
||||||
return "deployed", nil
|
return "deployed", nil
|
||||||
|
@ -243,20 +194,12 @@ var commands = []Command{
|
||||||
return "", fmt.Errorf("expected %s", requiredArguments[0])
|
return "", fmt.Errorf("expected %s", requiredArguments[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
var game, gameOpenError = OpenGame(arguments[0])
|
if game, openGameError := LoadGame(arguments[0]); openGameError == nil {
|
||||||
|
if cleanError := game.Clean(); cleanError != nil {
|
||||||
if gameOpenError != nil {
|
return "", cleanError
|
||||||
return "", gameOpenError
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
defer func() {
|
return "", openGameError
|
||||||
if closeError := game.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
if cleanDeployedModsError := game.CleanDeployedMods(); cleanDeployedModsError != nil {
|
|
||||||
return "", cleanDeployedModsError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "cleaned", nil
|
return "cleaned", nil
|
||||||
|
|
534
manager.go
534
manager.go
|
@ -13,29 +13,23 @@ import (
|
||||||
"sauce.pizzawednes.day/kayomn/ini-gusher"
|
"sauce.pizzawednes.day/kayomn/ini-gusher"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (game *Game) CleanDeployedMods() error {
|
func (game *Game) Clean() error {
|
||||||
// Clean up currently deployed files first.
|
// Clean up currently deployed files first.
|
||||||
for _, filePath := range game.DeployedFilePaths {
|
for _, filePath := range game.DeployedFilePaths {
|
||||||
var gameFilePath = filepath.Join(game.Path, filePath)
|
if removeError := os.Remove(filepath.Join(
|
||||||
|
game.Path, filePath)); (removeError != nil) && (!(os.IsNotExist(removeError))) {
|
||||||
|
|
||||||
if removeError := os.Remove(gameFilePath); removeError != nil {
|
|
||||||
if !(os.IsNotExist(removeError)) {
|
|
||||||
return removeError
|
return removeError
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
game.DeployedFilePaths = game.DeployedFilePaths[:0]
|
game.DeployedFilePaths = game.DeployedFilePaths[:0]
|
||||||
|
|
||||||
|
var overwriteDirPath = filepath.Join(cachePath(), game.Name, "overwritten")
|
||||||
|
|
||||||
// Then restore all files overwritten by previously deployment.
|
// Then restore all files overwritten by previously deployment.
|
||||||
for _, filePath := range game.OverwrittenFilePaths {
|
for _, filePath := range game.OverwrittenFilePaths {
|
||||||
var backupDirPath, backupDirPathError = game.cachePath("overwritten")
|
if renameError := os.Rename(filepath.Join(overwriteDirPath, filePath),
|
||||||
|
|
||||||
if backupDirPathError != nil {
|
|
||||||
return backupDirPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
if renameError := os.Rename(filepath.Join(backupDirPath, filePath),
|
|
||||||
filepath.Join(game.Path, filePath)); renameError != nil {
|
filepath.Join(game.Path, filePath)); renameError != nil {
|
||||||
|
|
||||||
return renameError
|
return renameError
|
||||||
|
@ -44,120 +38,32 @@ func (game *Game) CleanDeployedMods() error {
|
||||||
|
|
||||||
game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0]
|
game.OverwrittenFilePaths = game.OverwrittenFilePaths[:0]
|
||||||
|
|
||||||
return nil
|
return game.saveDeployment()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) Close() error {
|
func (game *Game) Deploy(names []string) error {
|
||||||
// Write deployed files to disk.
|
for _, name := range names {
|
||||||
var deployedListPath, deployedListPathError = game.cachePath("deployed.txt")
|
|
||||||
|
|
||||||
if deployedListPathError != nil {
|
|
||||||
return deployedListPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
var deployedListFile, deployedListCreateError = os.Create(deployedListPath)
|
|
||||||
|
|
||||||
if deployedListCreateError != nil {
|
|
||||||
return deployedListCreateError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if syncError := deployedListFile.Sync(); syncError != nil {
|
|
||||||
panic(syncError)
|
|
||||||
}
|
|
||||||
|
|
||||||
if closeError := deployedListFile.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var deployedListWriter = bufio.NewWriter(deployedListFile)
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if flushError := deployedListWriter.Flush(); flushError != nil {
|
|
||||||
panic(flushError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
for _, filePath := range game.DeployedFilePaths {
|
|
||||||
if _, writeError := deployedListWriter.WriteString(filePath); writeError != nil {
|
|
||||||
return writeError
|
|
||||||
}
|
|
||||||
|
|
||||||
if writeError := deployedListWriter.WriteByte('\n'); writeError != nil {
|
|
||||||
return writeError
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read mod info from disk.
|
|
||||||
var modInfoPath, modInfoPathError = game.configPath("mods.ini")
|
|
||||||
|
|
||||||
if modInfoPathError != nil {
|
|
||||||
return modInfoPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
var modInfoFile, modInfoCreateError = os.Create(modInfoPath)
|
|
||||||
|
|
||||||
if modInfoCreateError != nil {
|
|
||||||
return modInfoCreateError
|
|
||||||
}
|
|
||||||
|
|
||||||
defer func() {
|
|
||||||
if closeError := modInfoFile.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var modInfoEntries = make([]ini.Entry, 0, len(game.Mods)*3)
|
|
||||||
|
|
||||||
for name, mod := range game.Mods {
|
|
||||||
modInfoEntries = append(modInfoEntries, ini.Entry{
|
|
||||||
Section: name,
|
|
||||||
Key: "source",
|
|
||||||
Value: mod.Source,
|
|
||||||
})
|
|
||||||
|
|
||||||
modInfoEntries = append(modInfoEntries, ini.Entry{
|
|
||||||
Section: name,
|
|
||||||
Key: "format",
|
|
||||||
Value: mod.Format,
|
|
||||||
})
|
|
||||||
|
|
||||||
modInfoEntries = append(modInfoEntries, ini.Entry{
|
|
||||||
Section: name,
|
|
||||||
Key: "version",
|
|
||||||
Value: mod.Version,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return ini.Write(modInfoFile, modInfoEntries)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (game *Game) DeployMod(name string) error {
|
|
||||||
var mod, exists = game.Mods[name]
|
var mod, exists = game.Mods[name]
|
||||||
|
|
||||||
if !(exists) {
|
if !(exists) {
|
||||||
return fmt.Errorf("mod does not exist: %s", name)
|
return fmt.Errorf("mod does not exist: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var archivePath, archivePathError = game.configPath(name)
|
var installPath = fmt.Sprintf("%s.%s",
|
||||||
|
filepath.Join(configPath(), game.Name, name), mod.Format)
|
||||||
if archivePathError != nil {
|
|
||||||
return archivePathError
|
|
||||||
}
|
|
||||||
|
|
||||||
switch mod.Format {
|
switch mod.Format {
|
||||||
case "zip":
|
case "zip":
|
||||||
archivePath += ".zip"
|
var zipReadCloser, openError = zip.OpenReader(installPath)
|
||||||
|
|
||||||
var zipReadCloser, zipReadCloserOpenError = zip.OpenReader(archivePath)
|
if openError != nil {
|
||||||
|
return openError
|
||||||
if zipReadCloserOpenError != nil {
|
|
||||||
return zipReadCloserOpenError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if closeError := zipReadCloser.Close(); closeError != nil {
|
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)
|
panic(closeError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -175,58 +81,65 @@ func (game *Game) DeployMod(name string) error {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
var file, openFileError = os.OpenFile(
|
// Backup up any pre-existing file before it is overwritten by Zip entry
|
||||||
deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
|
// extraction.
|
||||||
|
if fileInfo, statError := os.Stat(deployPath); statError == nil {
|
||||||
|
var backupPath = filepath.Join(
|
||||||
|
cachePath(), game.Name, "overwritten", zipFile.Name)
|
||||||
|
|
||||||
if openFileError != nil {
|
if dirError := os.MkdirAll(filepath.Dir(backupPath), fileInfo.Mode()); dirError != nil {
|
||||||
if !(os.IsExist(openFileError)) {
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
||||||
return openFileError
|
return closeError
|
||||||
}
|
}
|
||||||
|
|
||||||
var backupPath, backupPathError = game.cachePath("overwritten") // deployPath
|
|
||||||
|
|
||||||
if backupPathError != nil {
|
|
||||||
return backupPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
backupPath = filepath.Join(backupPath, zipFile.Name)
|
|
||||||
|
|
||||||
if dirError := os.MkdirAll(filepath.Dir(backupPath), 0755); dirError != nil {
|
|
||||||
return dirError
|
return dirError
|
||||||
}
|
}
|
||||||
|
|
||||||
if renameError := os.Rename(deployPath, backupPath); renameError != nil {
|
if renameError := os.Rename(deployPath, backupPath); renameError != nil {
|
||||||
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
||||||
|
return closeError
|
||||||
|
}
|
||||||
|
|
||||||
return renameError
|
return renameError
|
||||||
}
|
}
|
||||||
|
|
||||||
game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name)
|
game.OverwrittenFilePaths = append(game.OverwrittenFilePaths, zipFile.Name)
|
||||||
|
} else if !(os.IsNotExist(statError)) {
|
||||||
file, openFileError = os.OpenFile(
|
if closeError := zipReadCloser.Close(); closeError != nil {
|
||||||
deployPath, os.O_WRONLY|os.O_CREATE|os.O_EXCL, fileMode)
|
return closeError
|
||||||
|
|
||||||
if openFileError != nil {
|
|
||||||
return openFileError
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var zipReadCloser, zipFileOpenError = zipFile.Open()
|
return statError
|
||||||
|
|
||||||
if zipFileOpenError != nil {
|
|
||||||
return zipFileOpenError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
if file, createError := os.Create(deployPath); createError == nil {
|
||||||
if syncError := file.Sync(); syncError != nil {
|
var zipFile, ioError = zipFile.Open()
|
||||||
panic(syncError)
|
|
||||||
|
if ioError == nil {
|
||||||
|
_, ioError = io.Copy(file, zipFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if closeError := file.Close(); closeError != nil {
|
if syncError := file.Sync(); (syncError != nil) && (ioError == nil) {
|
||||||
panic(closeError)
|
ioError = syncError
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
|
|
||||||
if _, copyError := io.Copy(file, zipReadCloser); copyError != nil {
|
if closeError := file.Close(); (closeError != nil) && (ioError == nil) {
|
||||||
return copyError
|
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)
|
game.DeployedFilePaths = append(game.DeployedFilePaths, zipFile.Name)
|
||||||
|
@ -235,8 +148,9 @@ func (game *Game) DeployMod(name string) error {
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported mod format: %s", mod.Format)
|
return fmt.Errorf("unsupported mod format: %s", mod.Format)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return game.saveDeployment()
|
||||||
}
|
}
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
|
@ -247,47 +161,60 @@ type Game struct {
|
||||||
Path string
|
Path string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) InstallMod(archivePath string) error {
|
func (game *Game) InstallMods(archivePaths []string) error {
|
||||||
var archiveBaseName = filepath.Base(archivePath)
|
var installDirPath = filepath.Join(configPath(), game.Name)
|
||||||
var archiveExtension = filepath.Ext(archiveBaseName)
|
|
||||||
|
for _, archivePath := range archivePaths {
|
||||||
|
var archiveName = filepath.Base(archivePath)
|
||||||
|
var archiveExtension = filepath.Ext(archiveName)
|
||||||
|
|
||||||
if len(archiveExtension) == 0 {
|
if len(archiveExtension) == 0 {
|
||||||
return fmt.Errorf("unknown archive format: %s", archiveExtension)
|
return fmt.Errorf("unknown archive format: %s", archiveExtension)
|
||||||
}
|
}
|
||||||
|
|
||||||
var name = strings.TrimSuffix(archiveBaseName, archiveExtension)
|
var name = strings.TrimSuffix(archiveName, archiveExtension)
|
||||||
|
|
||||||
if _, exists := game.Mods[name]; exists {
|
if _, exists := game.Mods[name]; exists {
|
||||||
return fmt.Errorf("mod with name already exists: `%s`", name)
|
return fmt.Errorf("mod with name already exists: `%s`", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy archive into installation directory.
|
// Copy archive into installation directory.
|
||||||
{
|
if archiveFile, openError := os.Open(archivePath); openError == nil {
|
||||||
var installPath, installPathError = game.configPath(archiveBaseName)
|
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)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if installPathError != nil {
|
var archiveFileInfo, statError = archiveFile.Stat()
|
||||||
return installPathError
|
|
||||||
|
if statError != nil {
|
||||||
|
return statError
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceFile, sourceOpenError = os.Open(archivePath)
|
if dirError := os.MkdirAll(installDirPath, archiveFileInfo.Mode()); dirError != nil {
|
||||||
|
return dirError
|
||||||
if sourceOpenError != nil {
|
|
||||||
return sourceOpenError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer sourceFile.Close()
|
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()
|
||||||
|
|
||||||
var targetFile, targetCreateError = os.Create(installPath)
|
if (copyError != nil) || (syncError != nil) {
|
||||||
|
return fmt.Errorf("failed to install mod")
|
||||||
if targetCreateError != nil {
|
|
||||||
return targetCreateError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
defer targetFile.Close()
|
if closeError != nil {
|
||||||
|
return closeError
|
||||||
if _, copyError := io.Copy(targetFile, sourceFile); copyError != nil {
|
|
||||||
return copyError
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return createError
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return openError
|
||||||
}
|
}
|
||||||
|
|
||||||
game.Mods[name] = Mod{
|
game.Mods[name] = Mod{
|
||||||
|
@ -295,43 +222,32 @@ func (game *Game) InstallMod(archivePath string) error {
|
||||||
Source: archivePath,
|
Source: archivePath,
|
||||||
Version: "",
|
Version: "",
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return game.saveMods()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) Load() error {
|
func (game *Game) Load() error {
|
||||||
// Read deployed files from disk.
|
// Read deployed files from disk.
|
||||||
var deployedListPath, deployedListPathError = game.cachePath("deployed.txt")
|
var deployedListPath = filepath.Join(cachePath(), game.Name, "deployed.txt")
|
||||||
|
|
||||||
if deployedListPathError != nil {
|
|
||||||
return deployedListPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
if file, openError := os.Open(deployedListPath); openError == nil {
|
if file, openError := os.Open(deployedListPath); openError == nil {
|
||||||
defer func() {
|
for scanner := bufio.NewScanner(file); scanner.Scan(); {
|
||||||
if closeError := file.Close(); closeError != nil {
|
|
||||||
panic(closeError)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
var scanner = bufio.NewScanner(file)
|
|
||||||
|
|
||||||
for scanner.Scan() {
|
|
||||||
game.DeployedFilePaths = append(game.DeployedFilePaths, scanner.Text())
|
game.DeployedFilePaths = append(game.DeployedFilePaths, scanner.Text())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if closeError := file.Close(); closeError != nil {
|
||||||
|
return closeError
|
||||||
|
}
|
||||||
} else if !(os.IsNotExist(openError)) {
|
} else if !(os.IsNotExist(openError)) {
|
||||||
return openError
|
return openError
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read overwritten files from disk.
|
// Read overwritten game files from disk.
|
||||||
var overwriteDirPath, overwriteDirPathError = game.cachePath("overwritten")
|
var overwrittenFilesDirPath = filepath.Join(cachePath(), game.Name, "overwritten")
|
||||||
|
|
||||||
if overwriteDirPathError != nil {
|
if _, statError := os.Stat(overwrittenFilesDirPath); statError == nil {
|
||||||
return overwriteDirPathError
|
if walkError := filepath.WalkDir(overwrittenFilesDirPath, func(
|
||||||
}
|
|
||||||
|
|
||||||
if _, statError := os.Stat(overwriteDirPath); statError == nil {
|
|
||||||
if walkError := filepath.WalkDir(overwriteDirPath, func(
|
|
||||||
path string, dirEntry fs.DirEntry, walkError error) error {
|
path string, dirEntry fs.DirEntry, walkError error) error {
|
||||||
|
|
||||||
if walkError != nil {
|
if walkError != nil {
|
||||||
|
@ -351,15 +267,11 @@ func (game *Game) Load() error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read mod info from disk.
|
// Read mod info from disk.
|
||||||
var modInfoPath, modInfoPathError = game.configPath("mods.ini")
|
if file, openError := os.Open(filepath.Join(configPath(), game.Name, "mods.ini")); openError == nil {
|
||||||
|
|
||||||
if modInfoPathError != nil {
|
|
||||||
return modInfoPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
if file, openError := os.Open(modInfoPath); openError == nil {
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if closeError := file.Close(); closeError != nil {
|
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)
|
panic(closeError)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
@ -399,7 +311,7 @@ type Mod struct {
|
||||||
Version string
|
Version string
|
||||||
}
|
}
|
||||||
|
|
||||||
func OpenGame(name string) (Game, error) {
|
func LoadGame(name string) (Game, error) {
|
||||||
var configPath, configPathError = os.UserConfigDir()
|
var configPath, configPathError = os.UserConfigDir()
|
||||||
|
|
||||||
if configPathError != nil {
|
if configPathError != nil {
|
||||||
|
@ -425,7 +337,7 @@ func OpenGame(name string) (Game, error) {
|
||||||
OverwrittenFilePaths: make([]string, 0, 512),
|
OverwrittenFilePaths: make([]string, 0, 512),
|
||||||
DeployedFilePaths: make([]string, 0, 512),
|
DeployedFilePaths: make([]string, 0, 512),
|
||||||
Mods: make(map[string]Mod),
|
Mods: make(map[string]Mod),
|
||||||
Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data",
|
Path: entry.Key,
|
||||||
}
|
}
|
||||||
|
|
||||||
if loadError := game.Load(); loadError != nil {
|
if loadError := game.Load(); loadError != nil {
|
||||||
|
@ -439,84 +351,93 @@ func OpenGame(name string) (Game, error) {
|
||||||
return Game{}, fmt.Errorf("game not registered: %s", name)
|
return Game{}, fmt.Errorf("game not registered: %s", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) RemoveMods(names []string) error {
|
func (game *Game) RemoveMod(name string) error {
|
||||||
for _, name := range names {
|
|
||||||
if _, exists := game.Mods[name]; !(exists) {
|
if _, exists := game.Mods[name]; !(exists) {
|
||||||
return fmt.Errorf("unknown mod: `%s`", name)
|
return fmt.Errorf("unknown mod: `%s`", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
var path, pathError = game.configPath(name)
|
if removeError := os.RemoveAll(filepath.Join(
|
||||||
|
configPath(), game.Name, name)); removeError != nil {
|
||||||
|
|
||||||
if pathError != nil {
|
|
||||||
return pathError
|
|
||||||
}
|
|
||||||
|
|
||||||
if removeError := os.RemoveAll(path); removeError != nil {
|
|
||||||
return removeError
|
return removeError
|
||||||
}
|
}
|
||||||
|
|
||||||
delete(game.Mods, name)
|
delete(game.Mods, name)
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return game.saveMods()
|
||||||
}
|
}
|
||||||
|
|
||||||
func RegisterGame(name string, dataPath string) error {
|
func RegisterGame(name string, dataPath string) error {
|
||||||
var configPath, configPathError = os.UserConfigDir()
|
var gamesPath = filepath.Join(configPath(), "games.ini")
|
||||||
|
var gameNamePaths = make(map[string]string)
|
||||||
|
|
||||||
if configPathError != nil {
|
if file, openError := os.Open(gamesPath); openError == nil {
|
||||||
return configPathError
|
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
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var gamesPath = filepath.Join(configPath, "modman", "games.ini")
|
if parserError := parser.Err(); parserError != nil {
|
||||||
|
return parserError
|
||||||
var gamesEntries = []ini.Entry{{
|
}
|
||||||
Section: name,
|
} else if !(os.IsNotExist(openError)) {
|
||||||
Key: "path",
|
return openError
|
||||||
Value: dataPath,
|
|
||||||
}}
|
|
||||||
|
|
||||||
if gamesFile, gamesFileError := os.Open(gamesPath); gamesFileError == nil {
|
|
||||||
var gamesParser = ini.NewParser(gamesFile)
|
|
||||||
|
|
||||||
for entry := gamesParser.Parse(); !(gamesParser.IsEnd()); entry = gamesParser.Parse() {
|
|
||||||
gamesEntries = append(gamesEntries, entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gamesFile.Close(); err != nil {
|
if file, updateError := os.Create(gamesPath); updateError == nil {
|
||||||
return err
|
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 err := gamesParser.Err(); err != nil {
|
if buildError := builder.KeyValue("path", path); buildError != nil {
|
||||||
return err
|
updateError = buildError
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
} else if !(os.IsNotExist(gamesFileError)) {
|
|
||||||
return gamesFileError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if gamesFile, gamesFileError := os.Create(gamesPath); gamesFileError == nil {
|
if writeError := writer.Flush(); (writeError != nil) && (updateError == nil) {
|
||||||
var writeError = ini.Write(gamesFile, gamesEntries)
|
updateError = writeError
|
||||||
|
|
||||||
if err := gamesFile.Sync(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := gamesFile.Close(); err != nil {
|
if syncError := file.Sync(); (syncError != nil) && (updateError == nil) {
|
||||||
return err
|
updateError = syncError
|
||||||
}
|
}
|
||||||
|
|
||||||
if writeError != nil {
|
if closeError := file.Close(); (closeError != nil) && (updateError == nil) {
|
||||||
return writeError
|
updateError = closeError
|
||||||
|
}
|
||||||
|
|
||||||
|
if updateError != nil {
|
||||||
|
return updateError
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return gamesFileError
|
return updateError
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) RenameMod(modName string, newName string) error {
|
func (game *Game) RenameMod(modName string, newName string) error {
|
||||||
if _, exists := game.Mods[modName]; !(exists) {
|
var mod, exists = game.Mods[modName]
|
||||||
|
|
||||||
|
if !(exists) {
|
||||||
return fmt.Errorf("no mod with that name exists")
|
return fmt.Errorf("no mod with that name exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -524,59 +445,134 @@ func (game *Game) RenameMod(modName string, newName string) error {
|
||||||
return fmt.Errorf("a mod with the new name already exists")
|
return fmt.Errorf("a mod with the new name already exists")
|
||||||
}
|
}
|
||||||
|
|
||||||
var modPath, modPathError = game.configPath(modName)
|
var modsDirPath = filepath.Join(configPath(), game.Name)
|
||||||
|
|
||||||
if modPathError != nil {
|
if renameError := os.Rename(filepath.Join(modsDirPath, modName),
|
||||||
return modPathError
|
filepath.Join(modsDirPath, newName)); renameError != nil {
|
||||||
}
|
|
||||||
|
|
||||||
var newPath, newPathError = game.configPath(modName)
|
|
||||||
|
|
||||||
if newPathError != nil {
|
|
||||||
return newPathError
|
|
||||||
}
|
|
||||||
|
|
||||||
if renameError := os.Rename(modPath, newPath); renameError != nil {
|
|
||||||
return renameError
|
return renameError
|
||||||
}
|
}
|
||||||
|
|
||||||
var mod = game.Mods[modName]
|
|
||||||
|
|
||||||
game.Mods[newName] = mod
|
game.Mods[newName] = mod
|
||||||
|
|
||||||
delete(game.Mods, modName)
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) cachePath(path string) (string, error) {
|
func (game *Game) saveMods() error {
|
||||||
var dirPath, pathError = os.UserCacheDir()
|
var file, updateError = os.Open(filepath.Join(configPath(), game.Name, "mods.ini"))
|
||||||
|
|
||||||
if pathError != nil {
|
if updateError != nil {
|
||||||
return "", pathError
|
return updateError
|
||||||
}
|
}
|
||||||
|
|
||||||
dirPath = filepath.Join(dirPath, "modman", game.Name)
|
var writer = bufio.NewWriter(file)
|
||||||
|
var builder = ini.NewBuilder(writer)
|
||||||
|
|
||||||
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
|
for name, mod := range game.Mods {
|
||||||
return "", mkdirError
|
if buildError := builder.Section(name); buildError != nil {
|
||||||
|
updateError = buildError
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(dirPath, path), nil
|
if buildError := builder.KeyValue("format", mod.Format); buildError != nil {
|
||||||
|
updateError = buildError
|
||||||
|
|
||||||
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) configPath(path string) (string, error) {
|
if buildError := builder.KeyValue("source", mod.Source); buildError != nil {
|
||||||
var dirPath, pathError = os.UserConfigDir()
|
updateError = buildError
|
||||||
|
|
||||||
if pathError != nil {
|
break
|
||||||
return "", pathError
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dirPath = filepath.Join(dirPath, "modman", game.Name)
|
if buildError := builder.KeyValue("version", mod.Version); buildError != nil {
|
||||||
|
updateError = buildError
|
||||||
|
|
||||||
if mkdirError := os.MkdirAll(dirPath, 0755); mkdirError != nil {
|
break
|
||||||
return "", mkdirError
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return filepath.Join(dirPath, path), nil
|
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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue