Parial work on clean and deploy

This commit is contained in:
kayomn 2022-12-05 13:13:58 +00:00
parent 3b92ee1421
commit 56d80c4883
2 changed files with 361 additions and 112 deletions

162
main.go
View File

@ -12,11 +12,13 @@ import (
type Command struct { type Command struct {
Name string Name string
Description string Description string
Action func([]string) (string, error) Action CommandAction
Arguments []string Arguments []string
IsVarargs bool IsVarargs bool
} }
type CommandAction func([]string, []string) (string, error)
var commands = []Command{ var commands = []Command{
{ {
Name: "install", Name: "install",
@ -24,38 +26,35 @@ var commands = []Command{
Arguments: []string{"game name", "archive path"}, Arguments: []string{"game name", "archive path"},
IsVarargs: true, IsVarargs: true,
Action: func(arguments []string) (string, error) { Action: func(requiredArguments []string, providedArguments []string) (string, error) {
var argumentCount = len(arguments) var argumentCount = len(providedArguments)
if argumentCount == 0 { if argumentCount < len(requiredArguments) {
return "", fmt.Errorf("expected game name") return "", fmt.Errorf("expected game name folowed by at least one archive path")
} }
var archivePaths = arguments[1:] var archivePaths = providedArguments[1:]
var processed = 0
if gameError := WithGame(arguments[0], func(game *Game) error { if gameError := WithGame(providedArguments[0], func(game *Game) error {
for i := range archivePaths { for i := range archivePaths {
var archivePath = archivePaths[i] var archivePath = archivePaths[i]
var extension = filepath.Ext(archivePath) var extension = filepath.Ext(archivePath)
if len(extension) == 0 { if len(extension) == 0 {
continue return fmt.Errorf("missing file extension: `%s`", archivePath)
} else {
extension = extension[1:]
} }
var extractor, extractorExists = extractors[extension] var extractor, extractorExists = extractors[extension[1:]]
if !(extractorExists) { if !(extractorExists) {
continue return fmt.Errorf("unsupported file format: `%s`", archivePath)
} }
if game.InstallMod(extractor, archivePath) != nil { if installError := game.InstallMod(
continue extractor, archivePath); installError != nil {
}
processed += 1 return installError
}
} }
return nil return nil
@ -63,7 +62,7 @@ var commands = []Command{
return "", gameError return "", gameError
} }
return fmt.Sprint(processed, " of ", len(archivePaths), " installed"), nil return "mods installed", nil
}, },
}, },
@ -73,31 +72,19 @@ var commands = []Command{
Arguments: []string{"game name", "mod name"}, Arguments: []string{"game name", "mod name"},
IsVarargs: true, IsVarargs: true,
Action: func(arguments []string) (string, error) { Action: func(requiredArguments []string, providedArguments []string) (string, error) {
var argumentCount = len(arguments) if len(providedArguments) < len(requiredArguments) {
return "", fmt.Errorf("expected %s followed by one or more %ss",
if argumentCount == 0 { requiredArguments[0], requiredArguments[1])
return "", fmt.Errorf("expected game name")
} }
var modsToRemove = arguments[1:] if gameError := WithGame(providedArguments[0], func(game *Game) error {
var removedMods = []string{} return game.RemoveMods(providedArguments[1:])
if gameError := WithGame(arguments[0], func(game *Game) error {
var removed, removeError = game.RemoveMods(modsToRemove)
if removeError != nil {
return removeError
}
removedMods = removed
return nil
}); gameError != nil { }); gameError != nil {
return "", gameError return "", gameError
} }
return fmt.Sprint(len(removedMods), " of ", len(removedMods), " removed"), nil return "removed mods", nil
}, },
}, },
@ -107,13 +94,16 @@ var commands = []Command{
Arguments: []string{"game name", "mod name", "new name"}, Arguments: []string{"game name", "mod name", "new name"},
IsVarargs: false, IsVarargs: false,
Action: func(arguments []string) (string, error) { Action: func(requiredArguments []string, providedArguments []string) (string, error) {
if len(arguments) != 3 { if len(providedArguments) != len(requiredArguments) {
return "", fmt.Errorf("expected game name followed by mod name and new name") return "", fmt.Errorf("expected %s followed by %s and %s",
requiredArguments[0], requiredArguments[1], requiredArguments[2])
} }
if gameError := WithGame(arguments[0], func(game *Game) error { if gameError := WithGame(providedArguments[0], func(game *Game) error {
if removeError := game.RenameMod(arguments[1], arguments[2]); removeError != nil { if removeError := game.RenameMod(providedArguments[1],
providedArguments[2]); removeError != nil {
return removeError return removeError
} }
@ -132,22 +122,23 @@ var commands = []Command{
Arguments: []string{"game name", "format"}, Arguments: []string{"game name", "format"},
IsVarargs: false, IsVarargs: false,
Action: func(arguments []string) (string, error) { Action: func(requiredArguments []string, providedArguments []string) (string, error) {
if len(arguments) != 2 { if len(providedArguments) != len(requiredArguments) {
return "", fmt.Errorf("expected game name followed by format") return "", fmt.Errorf("expected %s followed by %s",
requiredArguments[0], requiredArguments[1])
} }
var modManifest = "" var modManifest = ""
if gameError := WithGame(arguments[0], func(game *Game) error { if gameError := WithGame(providedArguments[0], func(game *Game) error {
var format = arguments[1] var format = providedArguments[1]
var formatter, formatterExists = formatters[format] var formatter, formatterExists = formatters[format]
if !(formatterExists) { if !(formatterExists) {
return fmt.Errorf("unsupported format: `%s`", format) return fmt.Errorf("unsupported format: `%s`", format)
} }
var formattedManifest, formatError = formatter(game.Mods) var formattedManifest, formatError = formatter(game.ModOrder)
if formatError != nil { if formatError != nil {
return formatError return formatError
@ -172,9 +163,76 @@ var commands = []Command{
Arguments: []string{"game name"}, Arguments: []string{"game name"},
IsVarargs: false, IsVarargs: false,
Action: func(arguments []string) (string, error) { Action: func(requiredArguments []string, arguments []string) (string, error) {
// TODO: Implement. if len(arguments) != len(requiredArguments) {
return "", fmt.Errorf("not implemented") return "", fmt.Errorf("expected %s", requiredArguments[0])
}
if gameError := WithGame(arguments[0], func(game *Game) error {
var deployError = game.Deploy()
if deployError != nil {
return deployError
}
return nil
}); gameError != nil {
return "", gameError
}
return "deployed", nil
},
},
{
Name: "disable",
Description: "Disable one or more installed mods",
Arguments: []string{"game name", "mod name"},
IsVarargs: true,
Action: func(requiredArguments []string, arguments []string) (string, error) {
if len(arguments) < len(requiredArguments) {
return "", fmt.Errorf("expected %s followed by one or more %ss",
requiredArguments[0], requiredArguments[1])
}
if gameError := WithGame(arguments[0], func(game *Game) error {
if enableError := game.SwitchMods(false, arguments[1:]); enableError != nil {
return enableError
}
return nil
}); gameError != nil {
return "", gameError
}
return "enabled", nil
},
},
{
Name: "enable",
Description: "Enable one or more installed mods",
Arguments: []string{"game name", "mod name"},
IsVarargs: true,
Action: func(requiredArguments []string, arguments []string) (string, error) {
if len(arguments) < len(requiredArguments) {
return "", fmt.Errorf("expected %s followed by one or more %ss",
requiredArguments[0], requiredArguments[1])
}
if gameError := WithGame(arguments[0], func(game *Game) error {
if enableError := game.SwitchMods(true, arguments[1:]); enableError != nil {
return enableError
}
return nil
}); gameError != nil {
return "", gameError
}
return "enabled", nil
}, },
}, },
} }
@ -291,7 +349,7 @@ func main() {
var command = commands[i] var command = commands[i]
if command.Name == commandName { if command.Name == commandName {
var response, actionError = command.Action(os.Args[2:]) var response, actionError = command.Action(command.Arguments, os.Args[2:])
if actionError != nil { if actionError != nil {
fmt.Fprintln(os.Stderr, actionError.Error()) fmt.Fprintln(os.Stderr, actionError.Error())

View File

@ -1,8 +1,11 @@
package main package main
import ( import (
"bufio"
"encoding/csv" "encoding/csv"
"fmt" "fmt"
"io"
"io/fs"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@ -12,33 +15,193 @@ type App struct {
ConfigDirPath string ConfigDirPath string
} }
func (game *Game) CachePath() (string, error) {
var path, pathError = os.UserCacheDir()
if pathError != nil {
return "", pathError
}
path = filepath.Join(path, "modman", game.ID)
if mkdirError := os.MkdirAll(path, 0755); mkdirError != nil {
return "", mkdirError
}
return path, nil
}
func (game *Game) Clean() error {
var cachePath, cachePathError = game.CachePath()
if cachePathError != nil {
return cachePathError
}
var deployedListPath = filepath.Join(cachePath, "backup", "deployed.txt")
if backupFile, openError := os.Open(deployedListPath); !(os.IsNotExist(openError)) {
defer backupFile.Close()
var scanner = bufio.NewScanner(backupFile)
for scanner.Scan() {
var
}
}
return nil
}
func (game *Game) ConfigPath() (string, error) {
var path, pathError = os.UserConfigDir()
if pathError != nil {
return "", pathError
}
path = filepath.Join(path, "modman", game.ID)
if mkdirError := os.MkdirAll(path, 0755); mkdirError != nil {
return "", mkdirError
}
return path, nil
}
func (game *Game) Deploy() error {
var cachePath, cachePathError = game.CachePath()
if cachePathError != nil {
return cachePathError
}
var configPath, configPathError = game.ConfigPath()
if configPathError != nil {
return configPathError
}
var backupPath = filepath.Join(cachePath, "backup")
var deployedListPath = filepath.Join(backupPath, "deployed.txt")
if backupFile, openError := os.Open(deployedListPath); !(os.IsNotExist(openError)) {
defer backupFile.Close()
var scanner = bufio.NewScanner(backupFile)
for scanner.Scan() {
var
}
}
var modsPath = filepath.Join(configPath, "mods")
for i := range game.ModOrder {
var mod = game.ModOrder[i]
if mod.IsEnabled {
var modPath = filepath.Join(modsPath, mod.Name)
if walkError := filepath.WalkDir(modPath, func(
path string, dirEntry fs.DirEntry, dirError error) error {
if dirError != nil {
return dirError
}
var fileMode = dirEntry.Type()
if !(fileMode.IsDir()) {
var localPath, relativeError = filepath.Rel(modPath, path)
if relativeError != nil {
return relativeError
}
var linkPath = filepath.Join(game.Path, localPath)
if pathError := os.MkdirAll(filepath.Dir(linkPath),
fileMode.Perm()); pathError != nil {
return pathError
}
if _, statError := os.Stat(linkPath); !(os.IsNotExist(statError)) {
var sourceFile, sourceOpenError = os.Open(linkPath)
if sourceOpenError != nil {
return sourceOpenError
}
defer sourceFile.Close()
var targetFile, targetOpenError = os.Create(backupPath)
if targetOpenError != nil {
return targetOpenError
}
defer targetFile.Close()
var _, copyError = io.Copy(targetFile, sourceFile)
if copyError != nil {
return copyError
}
}
if linkError := os.Link(path, linkPath); linkError != nil {
return linkError
}
}
return nil
}); walkError != nil {
return walkError
}
}
}
return nil
}
type Extractor func(string, string) error type Extractor func(string, string) error
type Game struct { type Game struct {
ID string ID string
Mods map[string]Mod ModOrder []Mod
ModNames map[string]int
Path string
HasUpdated bool HasUpdated bool
} }
func (game *Game) InstallMod(extractor Extractor, archivePath string) error { func (game *Game) InstallMod(extractor Extractor, archivePath string) error {
var gamePath, pathError = game.Path() var baseName = filepath.Base(archivePath)
var modName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
if _, exists := game.ModNames[modName]; exists {
return fmt.Errorf("mod with name already exists: `%s`", modName)
}
var configPath, pathError = game.ConfigPath()
if pathError != nil { if pathError != nil {
return pathError return pathError
} }
var baseName = filepath.Base(archivePath)
var modName = strings.TrimSuffix(baseName, filepath.Ext(baseName))
if extractError := extractor(archivePath, filepath.Join( if extractError := extractor(archivePath, filepath.Join(
gamePath, "mods", modName)); extractError != nil { configPath, "mods", modName)); extractError != nil {
return extractError return extractError
} }
game.Mods[modName] = Mod{ game.ModNames[modName] = len(game.ModOrder)
game.ModOrder = append(game.ModOrder, Mod{
IsEnabled: false, IsEnabled: false,
} Name: modName,
})
game.HasUpdated = true game.HasUpdated = true
@ -47,6 +210,30 @@ func (game *Game) InstallMod(extractor Extractor, archivePath string) error {
type Mod struct { type Mod struct {
IsEnabled bool IsEnabled bool
Name string
Source string
}
func (game *Game) SwitchMods(isEnabled bool, names []string) error {
if len(names) != 0 {
for i := range names {
var name = names[i]
var index, exists = game.ModNames[name]
if !(exists) {
return fmt.Errorf("mod does not exist: `%s`", name)
}
var mod = game.ModOrder[index]
mod.IsEnabled = isEnabled
game.ModOrder[index] = mod
}
game.HasUpdated = true
}
return nil
} }
func WithGame(gameName string, action func(*Game) error) error { func WithGame(gameName string, action func(*Game) error) error {
@ -58,17 +245,19 @@ func WithGame(gameName string, action func(*Game) error) error {
if gameName == supportedGame { if gameName == supportedGame {
var game = Game{ var game = Game{
ID: supportedGame, ID: supportedGame,
Mods: make(map[string]Mod), ModOrder: make([]Mod, 0, 512),
ModNames: make(map[string]int),
HasUpdated: false, HasUpdated: false,
Path: "/home/kayomn/.steam/steam/steamapps/common/Fallout 4/Data",
} }
var gamePath, pathError = game.Path() var configPath, pathError = game.ConfigPath()
if pathError != nil { if pathError != nil {
return pathError return pathError
} }
var manifestPath = filepath.Join(gamePath, "mods.csv") var manifestPath = filepath.Join(configPath, "mods.csv")
// Load manifest from disk. // Load manifest from disk.
{ {
@ -92,9 +281,13 @@ func WithGame(gameName string, action func(*Game) error) error {
var status = recordValues[0] var status = recordValues[0]
var name = recordValues[1] var name = recordValues[1]
game.Mods[name] = Mod{ game.ModNames[name] = len(game.ModOrder)
game.ModOrder = append(game.ModOrder, Mod{
IsEnabled: status == "*", IsEnabled: status == "*",
} Name: name,
Source: "",
})
recordValues, recordError = manifestReader.Read() recordValues, recordError = manifestReader.Read()
} }
@ -107,8 +300,7 @@ func WithGame(gameName string, action func(*Game) error) error {
// Save manifest back to disk. // Save manifest back to disk.
if game.HasUpdated { if game.HasUpdated {
var manifestFile, openError = os.OpenFile(manifestPath, var manifestFile, openError = os.Create(manifestPath)
os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
if openError != nil { if openError != nil {
return openError return openError
@ -118,14 +310,15 @@ func WithGame(gameName string, action func(*Game) error) error {
var manifestWriter = csv.NewWriter(manifestFile) var manifestWriter = csv.NewWriter(manifestFile)
for name, mod := range game.Mods { for i := range game.ModOrder {
var mod = game.ModOrder[i]
var status = "" var status = ""
if mod.IsEnabled { if mod.IsEnabled {
status = "*" status = "*"
} }
manifestWriter.Write([]string{status, name}) manifestWriter.Write([]string{status, mod.Name, mod.Source})
} }
manifestWriter.Flush() manifestWriter.Flush()
@ -138,65 +331,63 @@ func WithGame(gameName string, action func(*Game) error) error {
return fmt.Errorf("%s: game not supported", gameName) return fmt.Errorf("%s: game not supported", gameName)
} }
func (game Game) Path() (string, error) { func (game *Game) RemoveMods(names []string) error {
var configDirPath, configError = os.UserConfigDir() var configPath, pathError = game.ConfigPath()
if configError != nil {
return "", configError
}
return filepath.Join(configDirPath, "modman", game.ID), nil
}
func (game *Game) RemoveMods(modNames []string) ([]string, error) {
var gamePath, pathError = game.Path()
if pathError != nil {
return nil, pathError
}
var processed = 0
for i := range modNames {
var modName = modNames[i]
if removeError := os.RemoveAll(filepath.Join(gamePath, modName)); removeError != nil {
return nil, removeError
}
delete(game.Mods, modName)
game.HasUpdated = true
processed += 1
}
return modNames[0:processed], nil
}
func (game *Game) RenameMod(modName string, newName string) error {
var gamePath, pathError = game.Path()
if pathError != nil { if pathError != nil {
return pathError return pathError
} }
if _, exists := game.Mods[modName]; !(exists) { if len(names) != 0 {
game.HasUpdated = true
for i := range names {
var name = names[i]
var index, exists = game.ModNames[name]
if !(exists) {
return fmt.Errorf("unknown mod: `%s`", name)
}
if removeError := os.RemoveAll(filepath.Join(configPath, name)); removeError != nil {
return removeError
}
game.ModOrder = append(game.ModOrder[:index], game.ModOrder[index+1:]...)
delete(game.ModNames, name)
}
}
return nil
}
func (game *Game) RenameMod(modName string, newName string) error {
var configPath, pathError = game.ConfigPath()
if pathError != nil {
return pathError
}
if _, exists := game.ModNames[modName]; !(exists) {
return fmt.Errorf("no mod with that name exists") return fmt.Errorf("no mod with that name exists")
} }
if _, exists := game.Mods[newName]; exists { if _, nameTaken := game.ModNames[newName]; nameTaken {
return fmt.Errorf("a mod with the new name already exists") return fmt.Errorf("a mod with the new name already exists")
} }
if renameError := os.Rename(filepath.Join(gamePath, modName), var modsPath = filepath.Join(configPath, "mods")
filepath.Join(gamePath, newName)); renameError != nil {
if renameError := os.Rename(filepath.Join(modsPath, modName),
filepath.Join(modsPath, newName)); renameError != nil {
return renameError return renameError
} }
game.Mods[newName] = game.Mods[modName] game.ModNames[newName] = game.ModNames[modName]
delete(game.Mods, modName) delete(game.ModNames, modName)
game.HasUpdated = true game.HasUpdated = true