diff --git a/cmd/root.go b/cmd/root.go index 43d559d..0fb9cf9 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -157,7 +157,7 @@ var rootCmd = &cobra.Command{ if !util.PathIsFile(path.Join(opts.ServerDir, opts.ServerFile)) { fmt.Println("Server file not present, downloading...\n(Point --server-dir and --server-file flags to an existing server file to skip this step.)") - inst := server.InstallerFromDeps(&index.Dependencies) + inst := server.InstallerFromDeps(&index.Deps) err := inst.Install(opts.ServerDir, opts.ServerFile) if err != nil { log.Fatalln(err) @@ -194,18 +194,19 @@ var rootCmd = &cobra.Command{ log.Fatalln(err) } - packInfo, err := update.BuildPackState(zipPath) + packState, err := update.BuildPackState(zipPath) if err != nil { - fmt.Println(err) + log.Fatalln(err) } - err = packInfo.Save(path.Join(opts.ServerDir, "modpack.json")) + err = packState.Save(opts.ServerDir) if err != nil { - fmt.Println(err) + log.Fatalln(err) } if modsUnclean { fmt.Println("There have been problems downloading mods, you probably have to fix some dependency problems manually!") } + fmt.Println("Done :) Have a nice day ✌️") }, } diff --git a/cmd/update.go b/cmd/update.go index 36f5c60..d537974 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -1,14 +1,10 @@ package cmd import ( - "fmt" "github.com/nothub/mrpack-install/update" "github.com/nothub/mrpack-install/update/backup" - "github.com/nothub/mrpack-install/util" "github.com/spf13/cobra" "log" - "os" - "path" ) func init() { @@ -54,95 +50,9 @@ var updateCmd = &cobra.Command{ if len(args) > 1 { version = args[1] } - index, zipPath := handleArgs(input, version, opts.ServerDir, opts.Host) - fmt.Println("Updating:", index.Name) - - newPackInfo, err := update.BuildPackState(zipPath) - if err != nil { - log.Fatalln(err) - } - - for filePath := range newPackInfo.Hashes { - util.AssertPathSafe(filePath, opts.ServerDir) - } - - err = newPackInfo.Save(path.Join(opts.ServerDir, "modpack.json.update")) - if err != nil { - log.Fatalln(err) - } - oldPackInfo, err := update.LoadPackState(path.Join(opts.ServerDir, "modpack.json")) - if err != nil { - log.Fatalln(err) - } - - // TODO: clean this up (phase 1: collect all required actions phase 2: execute backups) (ignore all deletions here and just overwrite later on?) - deletions, updates, err := update.CompareModPackInfo(*oldPackInfo, *newPackInfo) - if err != nil { - return - } - deletionActions := update.GetDeletionActions(deletions, opts.ServerDir) - updateActions := update.GetUpdateActions(updates, opts.ServerDir) - - reportChanges(deletionActions, updateActions) - if !askContinue() { - fmt.Println("Update process canceled.") - return - } - - err = update.HandleOldFiles(deletionActions, opts.ServerDir) - if err != nil { - log.Fatalln(err) - } - - err = update.Do(updateActions, updates.Hashes, opts.ServerDir, zipPath, opts.DownloadThreads, opts.RetryTimes) - if err != nil { - log.Fatalln(err) - } - util.RemoveEmptyDirs(opts.ServerDir) - - err = os.Rename(path.Join(opts.ServerDir, "modpack.json.update"), path.Join(opts.ServerDir, "modpack.json")) - if err != nil { - log.Fatalln(err) - } + index, zipPath := handleArgs(input, version, opts.ServerDir, opts.Host) - fmt.Println("Done :) Have a nice day ✌️") + update.Cmd(opts, index, zipPath) }, } - -func reportChanges(deletions update.Actions, updates update.Actions) { - var changes update.Actions - for filePath, strategy := range deletions { - changes[filePath] = strategy - } - for filePath, strategy := range updates { - changes[filePath] = strategy - } - // TODO: include overrides in change report - - fmt.Printf("The following %v changes will be applied:\n", len(changes)) - for filePath, strategy := range changes { - switch strategy { - case update.Delete: - log.Printf("Delete and replace: %s\n", filePath) - case update.Backup: - log.Printf("Backup and replace: %s\n", filePath) - case update.NoOp: - log.Printf("Create new file: %s\n", filePath) - } - } -} - -func askContinue() bool { - fmt.Printf("Would you like to continue? [y/n]") - var input string - _, err := fmt.Scanln(&input) - if err != nil { - log.Fatalln(err) - } - if input == "y" { - return true - } - fmt.Println("Stopping process.") - return false -} diff --git a/go.mod b/go.mod index 503c9f6..27ac04d 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,7 @@ go 1.19 require ( github.com/google/uuid v1.3.0 - github.com/nothub/hashutils v0.3.0 + github.com/nothub/hashutils v0.4.0 github.com/spf13/cobra v1.6.1 ) diff --git a/go.sum b/go.sum index 601b805..622c570 100644 --- a/go.sum +++ b/go.sum @@ -3,8 +3,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/nothub/hashutils v0.3.0 h1:GCgn/vN6mjpZFd1U5s/lbYDmqXYPFqmGb+p+AdjCk8A= -github.com/nothub/hashutils v0.3.0/go.mod h1:nAmq0uaO7NHP5obrR5EpPhUEM8W8/uJbczWCQlzmCS4= +github.com/nothub/hashutils v0.4.0 h1:/lQiOrqXreZ8HRvNdhVZxyhVlhb7Pk3lXhicropKzow= +github.com/nothub/hashutils v0.4.0/go.mod h1:nAmq0uaO7NHP5obrR5EpPhUEM8W8/uJbczWCQlzmCS4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= github.com/spf13/cobra v1.6.1/go.mod h1:IOw/AERYS7UzyrGinqmz6HLUo219MORXGxhbaJUqzrY= diff --git a/modrinth/api/model.go b/modrinth/api/model.go index 673f372..3dcab52 100644 --- a/modrinth/api/model.go +++ b/modrinth/api/model.go @@ -142,6 +142,7 @@ type File struct { Size int `json:"size"` } +// Hashes are hex encoded. type Hashes struct { Sha1 string `json:"sha1"` Sha512 string `json:"sha512"` diff --git a/modrinth/mrpack/index.go b/modrinth/mrpack/index.go index 5c5748c..ac88051 100644 --- a/modrinth/mrpack/index.go +++ b/modrinth/mrpack/index.go @@ -11,13 +11,24 @@ import ( import modrinth "github.com/nothub/mrpack-install/modrinth/api" type Index struct { - FormatVersion int `json:"formatVersion"` - Game Game `json:"game"` - VersionId string `json:"versionId"` - Name string `json:"name"` - Summary string `json:"summary"` - Files []File `json:"files"` - Dependencies Dependencies `json:"dependencies"` + Format int `json:"formatVersion"` + Game Game `json:"game"` + Version string `json:"versionId"` + Name string `json:"name"` + Summary string `json:"summary"` + Files []File `json:"files"` + Deps Deps `json:"dependencies"` +} + +func (index *Index) ServerDls() []File { + var dls []File + for _, f := range index.Files { + if f.Env.Server == modrinth.UnsupportedEnvSupport { + continue + } + dls = append(dls, f) + } + return dls } type Game string @@ -39,7 +50,7 @@ type Env struct { Server modrinth.EnvSupport `json:"server"` } -type Dependencies struct { +type Deps struct { Minecraft string `json:"minecraft"` Fabric string `json:"fabric-loader"` Quilt string `json:"quilt-loader"` diff --git a/server/installer.go b/server/installer.go index 1cc30ce..3eabb53 100644 --- a/server/installer.go +++ b/server/installer.go @@ -9,7 +9,7 @@ type Installer interface { Install(serverDir string, serverFile string) error } -func InstallerFromDeps(deps *mrpack.Dependencies) Installer { +func InstallerFromDeps(deps *mrpack.Deps) Installer { var flavor Flavor if deps.Fabric != "" { flavor = Fabric diff --git a/update/actions.go b/update/actions.go index 84c03bb..b32da5e 100644 --- a/update/actions.go +++ b/update/actions.go @@ -7,24 +7,22 @@ import ( "github.com/nothub/hashutils/chksum" "github.com/nothub/hashutils/encoding" "github.com/nothub/mrpack-install/requester" - "github.com/nothub/mrpack-install/update/backup" "github.com/nothub/mrpack-install/util" "io" - "log" "os" "path/filepath" "strings" ) -type Strategy uint8 +type strategy uint8 const ( - Delete Strategy = iota + Delete strategy = iota Backup NoOp ) -// GetFileStrategy selects one of 3 strategies: +// GetStrategy selects one of 3 strategies for handling old files: // // 1. NoOp - File does not exist // @@ -33,7 +31,8 @@ const ( // 3. Backup - File exists but hash values do not match // // Hash must be sha512 and hex encoded. -func GetFileStrategy(hash string, path string) Strategy { +func GetStrategy(hash string, path string) strategy { + // TODO: replace with ShouldBackup if !util.PathIsFile(path) { return NoOp } @@ -45,62 +44,24 @@ func GetFileStrategy(hash string, path string) Strategy { } } -type Actions map[string]Strategy - -func GetDeletionActions(deletions *PackState, serverDir string) Actions { - actions := make(Actions) - for filePath := range deletions.Hashes { - switch GetFileStrategy(deletions.Hashes[filePath], filepath.Join(serverDir, filePath)) { - case Delete: - actions[filePath] = Delete - case Backup: - actions[filePath] = Backup - } +func ShouldBackup(path string, hash string) bool { + if !util.PathIsFile(path) { + return false } - return actions -} - -func GetUpdateActions(updates *PackState, serverDir string) Actions { - actions := make(Actions) - for filePath := range updates.Hashes { - switch GetFileStrategy(updates.Hashes[filePath], filepath.Join(serverDir, filePath)) { - case Delete: - delete(updates.Hashes, filePath) - case Backup: - err := backup.Create(filePath, serverDir) - if err != nil { - log.Fatalln(err.Error()) - } - } + match, _ := chksum.VerifyFile(path, hash, crypto.SHA512.New(), encoding.Hex) + if match { + return false + } else { + return true } - return actions } -func HandleOldFiles(deletions Actions, serverDir string) error { - for filePath, strategy := range deletions { - switch strategy { - case Delete: - log.Println("Delete", filePath) - err := os.Remove(filepath.Join(serverDir, filePath)) - if err != nil { - return err - } - case Backup: - err := backup.Create(filePath, serverDir) - if err != nil { - return err - } - } - } - return nil -} +func Do(newFiles []File, serverDir string, zipPath string, threads int, retries int) error { -func Do(updates Actions, hashes map[string]string, serverDir string, zipPath string, threads int, retries int) error { var downloads []*requester.Download downloadPools := requester.NewDownloadPools(requester.DefaultHttpClient, downloads, threads, retries) - // TODO: combine "updates Actions" and "hashes map[string]string" to a struct that also includes download links - for filePath := range updates { + for filePath := range newFiles { downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(hashes[filePath].DownloadLink, map[string]string{"sha1": hashes[filePath]}, filepath.Base(filePath), filepath.Join(serverDir, filepath.Dir(filePath)))) } downloadPools.Do() diff --git a/update/command.go b/update/command.go new file mode 100644 index 0000000..834f57d --- /dev/null +++ b/update/command.go @@ -0,0 +1,94 @@ +package update + +import ( + "fmt" + "github.com/nothub/mrpack-install/cmd" + "github.com/nothub/mrpack-install/modrinth/mrpack" + "github.com/nothub/mrpack-install/update/backup" + "github.com/nothub/mrpack-install/util" + "log" + "os" + "path/filepath" + "reflect" +) + +func Cmd(opts *cmd.UpdateOpts, index *mrpack.Index, zipPath string) { + fmt.Println("Updating:", index.Name) + + newState, err := BuildPackState(zipPath) + if err != nil { + log.Fatalln(err) + } + for filePath := range newState.Files { + util.AssertPathSafe(filePath, opts.ServerDir) + } + + oldState, err := LoadPackState(opts.ServerDir) + if err != nil { + log.Fatalln(err) + } + + if !reflect.DeepEqual(oldState.Deps, newState.Deps) { + // TODO: server update + log.Fatalln("mismatched versions, please upgrade manually") + } + + for path := range oldState.Files { + // ignore unchanged files + if newState.Files[path] == oldState.Files[path] { + delete(oldState.Files, path) + delete(newState.Files, path) + } + // skip deletion of old files that we overwrite with new files + if _, found := newState.Files[path]; found { + delete(oldState.Files, path) + } + } + + // handle old files + for path := range oldState.Files { + switch GetStrategy(oldState.Files[path], filepath.Join(opts.ServerDir, path)) { + case Delete: + err := os.Remove(filepath.Join(opts.ServerDir, path)) + if err != nil { + log.Fatalln(err.Error()) + } + case Backup: + err := backup.Create(path, opts.ServerDir) + if err != nil { + log.Fatalln(err.Error()) + } + } + } + + // new files + var newFiles []File + for path := range newState.Files { + var f File + f.Path = path + switch GetStrategy(newState.Files[path], filepath.Join(opts.ServerDir, path)) { + case Delete: + delete(newState.Files, path) + case Backup: + err := backup.Create(path, opts.ServerDir) + if err != nil { + log.Fatalln(err.Error()) + } + } + newFiles = append(newFiles, f) + } + + err = Do(newFiles, opts.ServerDir, zipPath, opts.DownloadThreads, opts.RetryTimes) + if err != nil { + log.Fatalln(err) + } + + util.RemoveEmptyDirs(opts.ServerDir) + + err = newState.Save(opts.ServerDir) + if err != nil { + log.Fatalln(err) + } + + fmt.Println("Done :) Have a nice day ✌️") +}