diff --git a/LICENSE.txt b/LICENSE.txt index 12d158a..8f5a78c 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright © 2022 hub +Copyright © 2023 hub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, diff --git a/cmd/root.go b/cmd/root.go index 9b55efa..cc4709f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -70,6 +70,7 @@ func GlobalOptions(cmd *cobra.Command) *GlobalOpts { log.Fatalln(err) } if proxy != "" { + // TODO: stop changing the default http client err := requester.DefaultHttpClient.SetProxy(proxy) if err != nil { log.Fatalln(err) @@ -122,112 +123,21 @@ var rootCmd = &cobra.Command{ mrpack-install yK0ISmKn 1.0.0-1.18 --server-dir mcserver mrpack-install communitypack9000 --host api.labrinth.example.org mrpack-install --version`, - Args: cobra.RangeArgs(0, 2), + Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { opts := GetRootOpts(cmd) - ver, err := cmd.Flags().GetBool("version") - if err != nil { - log.Fatalln(err) - } - if ver { + if opts.Version { fmt.Println("mrpack-install", buildinfo.Version) return } - if len(args) < 1 { - err = cmd.Help() - if err != nil { - fmt.Println(err) - } - os.Exit(1) - } input := args[0] version := "" if len(args) > 1 { version = args[1] } - - err = os.MkdirAll(opts.ServerDir, 0755) - if err != nil { - log.Fatalln(err) - } - - archivePath := "" - if util.PathIsFile(input) { - archivePath = input - - } else if util.IsValidUrl(input) { - fmt.Println("Downloading mrpack file from", args) - file, err := requester.DefaultHttpClient.DownloadFile(input, opts.ServerDir, "") - if err != nil { - log.Fatalln(err) - } - archivePath = file - defer func(name string) { - err := os.Remove(name) - if err != nil { - fmt.Println(err) - } - }(archivePath) - - } else { // input is project id or slug? - versions, err := modrinth.NewClient(opts.Host).GetProjectVersions(input, nil) - if err != nil { - log.Fatalln(err) - } - - // get files uploaded for specified version or latest stable if not specified - var files []modrinth.File = nil - for i := range versions { - if version != "" { - if versions[i].VersionNumber == version { - files = versions[i].Files - break - } - } else { - if versions[i].VersionType == modrinth.ReleaseVersionType { - files = versions[i].Files - break - } - } - } - if len(files) == 0 { - log.Fatalln("No files found for", input, version) - } - - for i := range files { - if strings.HasSuffix(files[i].Filename, ".mrpack") { - fmt.Println("Downloading mrpack file from", files[i].Url) - file, err := requester.DefaultHttpClient.DownloadFile(files[i].Url, opts.ServerDir, "") - if err != nil { - log.Fatalln(err) - } - archivePath = file - break - } - } - if archivePath == "" { - log.Fatalln("No mrpack file found for", input, version) - } - defer func(name string) { - err := os.Remove(name) - if err != nil { - fmt.Println(err) - } - }(archivePath) - } - - if archivePath == "" { - log.Fatalln("An error occured!") - } - - fmt.Println("Processing mrpack file", archivePath) - - index, err := mrpack.ReadIndex(archivePath) - if err != nil { - log.Fatalln(err) - } + index, archivePath := handleArgs(input, version, opts.ServerDir, opts.Host) for _, file := range index.Files { ok, err := util.PathIsSubpath(file.Path, opts.ServerDir) @@ -240,36 +150,13 @@ var rootCmd = &cobra.Command{ } fmt.Println("Installing:", index.Name) - fmt.Printf("Flavor dependencies: %+v\n", index.Dependencies) // download server if not present 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.)") - var provider server.Provider - if index.Dependencies.Fabric != "" { - provider, err = server.NewProvider("fabric", index.Dependencies.Minecraft, index.Dependencies.Fabric) - if err != nil { - log.Fatalln(err) - } - } else if index.Dependencies.Quilt != "" { - provider, err = server.NewProvider("quilt", index.Dependencies.Minecraft, index.Dependencies.Quilt) - if err != nil { - log.Fatalln(err) - } - } else if index.Dependencies.Forge != "" { - provider, err = server.NewProvider("forge", index.Dependencies.Minecraft, index.Dependencies.Forge) - if err != nil { - log.Fatalln(err) - } - } else { - provider, err = server.NewProvider("vanilla", index.Dependencies.Minecraft, "") - if err != nil { - log.Fatalln(err) - } - } - - err = provider.Provide(opts.ServerDir, opts.ServerFile) + inst := server.InstallerFromDeps(&index.Dependencies) + err := inst.Install(opts.ServerDir, opts.ServerFile) if err != nil { log.Fatalln(err) } @@ -285,7 +172,7 @@ var rootCmd = &cobra.Command{ if file.Env.Server == modrinth.UnsupportedEnvSupport { continue } - downloads = append(downloads, requester.NewDownload(file.Downloads, map[string]string{"sha1": string(file.Hashes.Sha1)}, filepath.Base(file.Path), path.Join(opts.ServerDir, filepath.Dir(file.Path)))) + downloads = append(downloads, requester.NewDownload(file.Downloads, map[string]string{"sha1": file.Hashes.Sha1}, filepath.Base(file.Path), path.Join(opts.ServerDir, filepath.Dir(file.Path)))) } downloadPools := requester.NewDownloadPools(requester.DefaultHttpClient, downloads, opts.DownloadThreads, opts.RetryTimes) downloadPools.Do() @@ -300,27 +187,130 @@ var rootCmd = &cobra.Command{ // overrides fmt.Println("Extracting overrides...") - err = mrpack.ExtractOverrides(archivePath, opts.ServerDir) + err := mrpack.ExtractOverrides(archivePath, opts.ServerDir) if err != nil { log.Fatalln(err) } - info, err := update.GenerateModPackInfo(index) + packInfo, err := update.GenerateModPackInfo(index) if err != nil { fmt.Println(err) } - err = info.Write(path.Join(opts.ServerDir, "modpack.json")) + err = packInfo.Write(path.Join(opts.ServerDir, "modpack.json")) if err != nil { fmt.Println(err) } if modsUnclean { - fmt.Println("There have been problems downloading downloading mods, you probably have to fix some dependency problems manually!") + fmt.Println("There have been problems downloading mods, you probably have to fix some dependency problems manually!") } fmt.Println("Done :) Have a nice day ✌️") }, } +func readArgs(args []string) (string, string) { + var input string + var version string + + if len(args) > 0 { + input = args[0] + } + + if len(args) > 1 { + version = args[1] + } + + return input, version +} + +func handleArgs(input string, version string, serverDir string, host string) (*mrpack.Index, string) { + err := os.MkdirAll(serverDir, 0755) + if err != nil { + log.Fatalln(err) + } + + archivePath := "" + if util.PathIsFile(input) { + archivePath = input + + } else if util.IsValidUrl(input) { + fmt.Println("Downloading mrpack file from", input) + file, err := requester.DefaultHttpClient.DownloadFile(input, serverDir, "") + if err != nil { + log.Fatalln(err.Error()) + } + archivePath = file + + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err.Error()) + } + }(archivePath) + + } else { + // input is project id or slug? + versions, err := modrinth.NewClient(host).GetProjectVersions(input, nil) + if err != nil { + log.Fatalln(err) + } + + var files []modrinth.File = nil + for i := range versions { + if version != "" { + if versions[i].VersionNumber == version { + files = versions[i].Files + break + } + } else { + // latest stable release if version not specified + if versions[i].VersionType == modrinth.ReleaseVersionType { + files = versions[i].Files + break + } + } + } + if len(files) == 0 { + log.Fatalln("No files found for", input, version) + } + + for i := range files { + if strings.HasSuffix(files[i].Filename, ".mrpack") { + fmt.Println("Downloading mrpack file from", files[i].Url) + file, err := requester.DefaultHttpClient.DownloadFile(files[i].Url, serverDir, "") + if err != nil { + // TODO: check next file on failure + log.Fatalln(err.Error()) + } + archivePath = file + break + } + } + + if archivePath == "" { + log.Fatalln("No mrpack file found for", input, version) + } + + defer func(name string) { + err := os.Remove(name) + if err != nil { + fmt.Println(err.Error()) + } + }(archivePath) + } + + if archivePath == "" { + log.Fatalln("Unable to handle input: ", input, version) + } + + index, err := mrpack.ReadIndex(archivePath) + if err != nil { + log.Fatalln(err.Error()) + } + + return index, archivePath +} + func Execute() { if rootCmd.Execute() != nil { os.Exit(1) diff --git a/cmd/server.go b/cmd/server.go index 0e4ed64..a6a6b0c 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -2,7 +2,6 @@ package cmd import ( "github.com/nothub/mrpack-install/mojang" - "github.com/nothub/mrpack-install/requester" "github.com/nothub/mrpack-install/server" "github.com/spf13/cobra" "log" @@ -20,61 +19,67 @@ func init() { rootCmd.AddCommand(serverCmd) } +type ServerOpts struct { + *GlobalOpts + MinecraftVersion string + FlavorVersion string +} + +func GetServerOpts(cmd *cobra.Command) *ServerOpts { + var opts ServerOpts + opts.GlobalOpts = GlobalOptions(cmd) + + minecraftVersion, err := cmd.Flags().GetString("minecraft-version") + if err != nil { + log.Fatalln(err) + } + if minecraftVersion == "" || minecraftVersion == "latest" { + latestMinecraftVersion, err := mojang.LatestRelease() + if err != nil { + log.Fatalln(err) + } + minecraftVersion = latestMinecraftVersion + } + opts.MinecraftVersion = minecraftVersion + + flavorVersion, err := cmd.Flags().GetString("flavor-version") + if err != nil { + log.Fatalln(err) + } + opts.FlavorVersion = flavorVersion + + return &opts +} + var serverCmd = &cobra.Command{ Use: "server (vanilla | fabric | quilt | forge | paper)", Short: "Prepare a plain server environment", Long: `Download and configure one of several Minecraft server flavors.`, Example: ` mrpack-install server fabric --server-dir fabric-srv mrpack-install server paper --minecraft-version 1.18.2 --server-file srv.jar`, - Args: cobra.ExactValidArgs(1), - ValidArgs: []string{"vanilla", "fabric", "quilt", "forge", "paper"}, + Args: cobra.MatchAll(cobra.ExactArgs(1), cobra.OnlyValidArgs), + ValidArgs: []string{ + server.Vanilla.String(), + server.Fabric.String(), + server.Quilt.String(), + server.Forge.String(), + server.Paper.String(), + }, Run: func(cmd *cobra.Command, args []string) { - minecraftVersion, err := cmd.Flags().GetString("minecraft-version") - if err != nil { - log.Fatalln(err) - } - flavorVersion, err := cmd.Flags().GetString("flavor-version") - if err != nil { - log.Fatalln(err) - } - serverDir, err := cmd.Flags().GetString("server-dir") - if err != nil { - log.Fatalln(err) - } - serverFile, err := cmd.Flags().GetString("server-file") - if err != nil { - log.Fatalln(err) - } - proxy, err := cmd.Flags().GetString("proxy") - if err != nil { - log.Fatalln(err) - } - if proxy != "" { - err := requester.DefaultHttpClient.SetProxy(proxy) - if err != nil { - log.Fatalln(err) - } - } + opts := GetServerOpts(cmd) - if minecraftVersion == "" || minecraftVersion == "latest" { - latestMinecraftVersion, err := mojang.LatestRelease() - if err != nil { - log.Fatalln(err) - } - minecraftVersion = latestMinecraftVersion - } - - err = os.MkdirAll(serverDir, 0755) + err := os.MkdirAll(opts.ServerDir, 0755) if err != nil { log.Fatalln(err) } - flavor := args[0] - provider, err := server.NewProvider(flavor, minecraftVersion, flavorVersion) + flavor := server.GetFlavor(args[0]) + inst, err := server.NewInstaller(flavor, opts.MinecraftVersion, opts.FlavorVersion) if err != nil { log.Fatalln(err) } - err = provider.Provide(serverDir, serverFile) + + err = inst.Install(opts.ServerDir, opts.ServerFile) if err != nil { log.Fatalln(err) } diff --git a/cmd/update.go b/cmd/update.go index 9d73cfc..a0a7ec8 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -2,16 +2,12 @@ package cmd import ( "fmt" - modrinth "github.com/nothub/mrpack-install/modrinth/api" - "github.com/nothub/mrpack-install/modrinth/mrpack" - "github.com/nothub/mrpack-install/requester" "github.com/nothub/mrpack-install/update" "github.com/nothub/mrpack-install/util" "github.com/spf13/cobra" "log" "os" "path" - "strings" ) func init() { @@ -24,108 +20,23 @@ var updateCmd = &cobra.Command{ Use: "update", Short: "Update the deployed modpack", Long: `Update the deployed modpacks config and mod files, creating backup files if necessary.`, + Args: cobra.RangeArgs(1, 2), Run: func(cmd *cobra.Command, args []string) { opts := GlobalOptions(cmd) - if len(args) < 1 { - err := cmd.Help() - if err != nil { - fmt.Println(err) - } - os.Exit(1) - } input := args[0] version := "" if len(args) > 1 { version = args[1] } + index, archivePath := handleArgs(input, version, opts.ServerDir, opts.Host) - err := os.MkdirAll(opts.ServerDir, 0755) + newPackInfo, err := update.GenerateModPackInfo(index) if err != nil { log.Fatalln(err) } - archivePath := "" - if util.PathIsFile(input) { - archivePath = input - - } else if util.IsValidUrl(input) { - fmt.Println("Downloading mrpack file from", args) - file, err := requester.DefaultHttpClient.DownloadFile(input, opts.ServerDir, "") - if err != nil { - log.Fatalln(err) - } - archivePath = file - defer func(name string) { - err := os.Remove(name) - if err != nil { - fmt.Println(err) - } - }(archivePath) - } else { // input is project id or slug? - versions, err := modrinth.NewClient(opts.Host).GetProjectVersions(input, nil) - if err != nil { - log.Fatalln(err) - } - - // get files uploaded for specified version or latest stable if not specified - var files []modrinth.File = nil - for i := range versions { - if version != "" { - if versions[i].VersionNumber == version { - files = versions[i].Files - break - } - } else { - if versions[i].VersionType == modrinth.ReleaseVersionType { - files = versions[i].Files - break - } - } - } - if len(files) == 0 { - log.Fatalln("No files found for", input, version) - } - - for i := range files { - if strings.HasSuffix(files[i].Filename, ".mrpack") { - fmt.Println("Downloading mrpack file from", files[i].Url) - file, err := requester.DefaultHttpClient.DownloadFile(files[i].Url, opts.ServerDir, "") - if err != nil { - log.Fatalln(err) - } - archivePath = file - break - } - } - if archivePath == "" { - log.Fatalln("No mrpack file found for", input, version) - } - defer func(name string) { - err := os.Remove(name) - if err != nil { - fmt.Println(err) - } - }(archivePath) - } - - if archivePath == "" { - log.Fatalln("An error occured!") - } - - fmt.Println("Processing mrpack file", archivePath) - - index, err := mrpack.ReadIndex(archivePath) - if err != nil { - log.Fatalln(err) - } - - newModPackInfo, err := update.GenerateModPackInfo(index) - if err != nil { - log.Fatalln(err) - } - - for path := range newModPackInfo.Hashes { + for path := range newPackInfo.Hashes { ok, err := util.PathIsSubpath(path, opts.ServerDir) if err != nil { log.Println(err.Error()) @@ -135,15 +46,17 @@ var updateCmd = &cobra.Command{ } } - err = newModPackInfo.Write(path.Join(opts.ServerDir, "modpack.json.update")) + fmt.Println("Updating:", index.Name) + + err = newPackInfo.Write(path.Join(opts.ServerDir, "modpack.json.update")) if err != nil { log.Fatalln(err) } - oldModPackInfo, err := update.ReadModPackInfo(path.Join(opts.ServerDir, "modpack.json")) + oldPackInfo, err := update.ReadModPackInfo(path.Join(opts.ServerDir, "modpack.json")) if err != nil { log.Fatalln(err) } - deleteFileInfo, updateFileInfo, err := update.CompareModPackInfo(*oldModPackInfo, *newModPackInfo) + deleteFileInfo, updateFileInfo, err := update.CompareModPackInfo(*oldPackInfo, *newPackInfo) if err != nil { return } @@ -166,10 +79,7 @@ var updateCmd = &cobra.Command{ log.Fatalln(err) } - var downloads []*requester.Download - downloadPools := requester.NewDownloadPools(requester.DefaultHttpClient, downloads, opts.DownloadThreads, opts.RetryTimes) - - err = update.ModPackUpdateDo(updateActions, updateFileInfo.Hashes, opts.ServerDir, archivePath, downloadPools) + err = update.ModPackUpdateDo(updateActions, updateFileInfo.Hashes, opts.ServerDir, archivePath, opts.DownloadThreads, opts.RetryTimes) if err != nil { log.Fatalln(err) } diff --git a/go.mod b/go.mod index 52f4dfc..00fc515 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.19 require ( github.com/google/uuid v1.3.0 + github.com/nothub/hashutils v0.2.0 github.com/spf13/cobra v1.6.1 ) diff --git a/go.sum b/go.sum index 137c42f..85be5e1 100644 --- a/go.sum +++ b/go.sum @@ -3,6 +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.2.0 h1:Gj4D0KNCpmvl9mthaC0tm1Lua2VqL8OkAM3NkymEc8k= +github.com/nothub/hashutils v0.2.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 5ff8ab6..673f372 100644 --- a/modrinth/api/model.go +++ b/modrinth/api/model.go @@ -1,6 +1,8 @@ package api -import "time" +import ( + "time" +) type ProjectType string @@ -141,8 +143,8 @@ type File struct { } type Hashes struct { - Sha512 HashAlgo `json:"sha512"` - Sha1 HashAlgo `json:"sha1"` + Sha1 string `json:"sha1"` + Sha512 string `json:"sha512"` } type Dependencies struct { diff --git a/modrinth/mrpack/overrides.go b/modrinth/mrpack/overrides.go index 73b0d64..f73a8e7 100644 --- a/modrinth/mrpack/overrides.go +++ b/modrinth/mrpack/overrides.go @@ -2,7 +2,10 @@ package mrpack import ( "archive/zip" + "crypto" "fmt" + "github.com/nothub/hashutils/chksum" + "github.com/nothub/hashutils/encoding" "github.com/nothub/mrpack-install/util" "io" "log" @@ -12,16 +15,23 @@ import ( "strings" ) -func GetOverrideHashes(zipFile string) (*util.Hashes, error) { - hashes := make(util.Hashes) +func extractedPath(file *zip.File) (bool, string) { + if strings.HasPrefix(file.Name, "overrides/") { + return true, strings.TrimPrefix(file.Name, "overrides/") + } + if strings.HasPrefix(file.Name, "server-overrides/") { + return true, strings.TrimPrefix(file.Name, "server-overrides/") + } + return false, "" +} + +func GetOverrideHashes(zipFile string) (*map[string]string, error) { + hashes := make(map[string]string) err := util.IterZip(zipFile, func(file *zip.File) error { - var name string - if strings.HasPrefix(file.Name, "overrides/") { - name = strings.TrimPrefix(file.Name, "overrides/") - } else if strings.HasPrefix(file.Name, "server-overrides/") { - name = strings.TrimPrefix(file.Name, "server-overrides/") - } else { + ok, p := extractedPath(file) + if !ok { + // skip non-server overrides return nil } @@ -30,17 +40,17 @@ func GetOverrideHashes(zipFile string) (*util.Hashes, error) { return err } - hash, err := util.GetReadCloserSha1(reader) + h, err := chksum.Create(reader, crypto.SHA512.New(), encoding.Hex) if err != nil { return err } err = reader.Close() if err != nil { - return err + log.Println(err.Error()) } - hashes[name] = hash + hashes[p] = h return nil }) @@ -53,7 +63,6 @@ func GetOverrideHashes(zipFile string) (*util.Hashes, error) { func ExtractOverrides(zipFile string, target string) error { err := util.IterZip(zipFile, func(file *zip.File) error { - filePath := file.Name if strings.HasPrefix(filePath, "overrides/") { filePath = strings.TrimPrefix(filePath, "overrides/") @@ -86,7 +95,8 @@ func ExtractOverrides(zipFile string, target string) error { if err != nil { return err } - if _, err := io.Copy(outFile, fileReader); err != nil { + _, err = io.Copy(outFile, fileReader) + if err != nil { return err } @@ -103,7 +113,6 @@ func ExtractOverrides(zipFile string, target string) error { return nil }) - if err != nil { log.Fatalln(err.Error()) } diff --git a/requester/multi_download.go b/requester/multi_download.go index c7cc451..30538a6 100644 --- a/requester/multi_download.go +++ b/requester/multi_download.go @@ -1,8 +1,10 @@ package requester import ( + "crypto" "fmt" - "github.com/nothub/mrpack-install/util" + "github.com/nothub/hashutils/chksum" + "github.com/nothub/hashutils/encoding" "sync" ) @@ -53,7 +55,7 @@ func (downloadPools *DownloadPools) Do() { // check hashcode if sha1code, ok := dl.hashes["sha1"]; ok { - _, err = util.CheckFileSha1(sha1code, f) + _, err = chksum.VerifyFile(f, sha1code, crypto.SHA1.New(), encoding.Hex) } if err != nil { fmt.Println("Hash check failed for:", dl.FileName, err, "attempt:", retries+1) diff --git a/server/fabric.go b/server/fabric.go index b467cca..b801623 100644 --- a/server/fabric.go +++ b/server/fabric.go @@ -7,15 +7,15 @@ import ( "net/url" ) -type Fabric struct { +type FabricInstaller struct { MinecraftVersion string FabricVersion string } -func (provider *Fabric) Provide(serverDir string, serverFile string) error { - loaderVersion := provider.FabricVersion +func (inst *FabricInstaller) Install(serverDir string, serverFile string) error { + loaderVersion := inst.FabricVersion if loaderVersion == "" || loaderVersion == "latest" { - latestLoaderVersion, err := latestFabricLoaderVersion(provider.MinecraftVersion) + latestLoaderVersion, err := latestFabricLoaderVersion(inst.MinecraftVersion) if err != nil { return err } @@ -27,7 +27,7 @@ func (provider *Fabric) Provide(serverDir string, serverFile string) error { return err } - versionTriple := provider.MinecraftVersion + "/" + loaderVersion + "/" + installerVersion + versionTriple := inst.MinecraftVersion + "/" + loaderVersion + "/" + installerVersion u, err := url.Parse("https://meta.fabricmc.net/v2/versions/loader/" + versionTriple + "/server/jar") if err != nil { return err diff --git a/server/flavors.go b/server/flavors.go new file mode 100644 index 0000000..dd6c295 --- /dev/null +++ b/server/flavors.go @@ -0,0 +1,33 @@ +package server + +import "strings" + +type Flavor string + +const ( + Vanilla Flavor = "vanilla" + Fabric Flavor = "fabric" + Quilt Flavor = "quilt" + Forge Flavor = "forge" + Paper Flavor = "paper" +) + +func GetFlavor(flavor string) Flavor { + switch strings.ToLower(flavor) { + case "vanilla": + return Vanilla + case "fabric": + return Fabric + case "quilt": + return Quilt + case "forge": + return Forge + case "paper": + return Paper + } + return "" +} + +func (f Flavor) String() string { + return string(f) +} diff --git a/server/forge.go b/server/forge.go index 6997027..4c5ecc0 100644 --- a/server/forge.go +++ b/server/forge.go @@ -5,13 +5,13 @@ import ( "fmt" ) -type Forge struct { +type ForgeInstaller struct { MinecraftVersion string ForgeVersion string } -func (provider *Forge) Provide(serverDir string, serverFile string) error { - u := "https://files.minecraftforge.net/net/minecraftforge/forge/index_" + provider.MinecraftVersion + ".html" - fmt.Println("Please acquire the required forge server file ("+provider.ForgeVersion+") manually to continue:", u) +func (inst *ForgeInstaller) Install(serverDir string, serverFile string) error { + u := "https://files.minecraftforge.net/net/minecraftforge/forge/index_" + inst.MinecraftVersion + ".html" + fmt.Println("Please acquire the required forge server file ("+inst.ForgeVersion+") manually to continue:", u) return errors.New("forge provider not implemented") } diff --git a/server/installer.go b/server/installer.go new file mode 100644 index 0000000..1cc30ce --- /dev/null +++ b/server/installer.go @@ -0,0 +1,60 @@ +package server + +import ( + "github.com/nothub/mrpack-install/modrinth/mrpack" + "log" +) + +type Installer interface { + Install(serverDir string, serverFile string) error +} + +func InstallerFromDeps(deps *mrpack.Dependencies) Installer { + var flavor Flavor + if deps.Fabric != "" { + flavor = Fabric + } else if deps.Quilt != "" { + flavor = Quilt + } else if deps.Forge != "" { + flavor = Forge + } else { + flavor = Vanilla + } + inst, err := NewInstaller(flavor, deps.Minecraft, "") + if err != nil { + log.Fatalln(err) + } + return inst +} + +func NewInstaller(flavor Flavor, minecraftVersion string, flavorVersion string) (Installer, error) { + var inst Installer = nil + switch flavor { + case Vanilla: + inst = &VanillaInstaller{ + MinecraftVersion: minecraftVersion, + } + case Fabric: + inst = &FabricInstaller{ + MinecraftVersion: minecraftVersion, + FabricVersion: flavorVersion, + } + case Quilt: + inst = &QuiltInstaller{ + MinecraftVersion: minecraftVersion, + QuiltVersion: flavorVersion, + } + case Forge: + inst = &ForgeInstaller{ + MinecraftVersion: minecraftVersion, + ForgeVersion: flavorVersion, + } + case Paper: + inst = &PaperInstaller{ + MinecraftVersion: minecraftVersion, + } + default: + log.Fatalln("No installer for flavor: " + flavor) + } + return inst, nil +} diff --git a/server/paper.go b/server/paper.go index 2aaf6bd..21e3b7d 100644 --- a/server/paper.go +++ b/server/paper.go @@ -7,11 +7,11 @@ import ( "strconv" ) -type Paper struct { +type PaperInstaller struct { MinecraftVersion string } -func (provider *Paper) Provide(serverDir string, serverFile string) error { +func (inst *PaperInstaller) Install(serverDir string, serverFile string) error { var response struct { Builds []struct { Id int `json:"build"` @@ -26,7 +26,7 @@ func (provider *Paper) Provide(serverDir string, serverFile string) error { } err := requester.DefaultHttpClient.GetJson("https://api.papermc.io/v2/projects/paper/versions/"+ - provider.MinecraftVersion+"/builds", &response, nil) + inst.MinecraftVersion+"/builds", &response, nil) if err != nil { return err } @@ -34,7 +34,7 @@ func (provider *Paper) Provide(serverDir string, serverFile string) error { for i := range response.Builds { i = len(response.Builds) - 1 - i if response.Builds[i].Channel == "default" { - u := "https://api.papermc.io/v2/projects/paper/versions/" + provider.MinecraftVersion + + u := "https://api.papermc.io/v2/projects/paper/versions/" + inst.MinecraftVersion + "/builds/" + strconv.Itoa(response.Builds[i].Id) + "/downloads/" + response.Builds[i].Downloads.Application.Name file, err := requester.DefaultHttpClient.DownloadFile(u, serverDir, serverFile) diff --git a/server/provider.go b/server/provider.go deleted file mode 100644 index 7eee705..0000000 --- a/server/provider.go +++ /dev/null @@ -1,46 +0,0 @@ -package server - -import ( - "errors" - "strings" -) - -type Provider interface { - Provide(serverDir string, serverFile string) error -} - -func NewProvider(flavor string, minecraftVersion string, flavorVersion string) (Provider, error) { - var provider Provider = nil - - switch strings.ToLower(flavor) { - case "vanilla": - provider = &Vanilla{ - MinecraftVersion: minecraftVersion, - } - case "fabric": - provider = &Fabric{ - MinecraftVersion: minecraftVersion, - FabricVersion: flavorVersion, - } - case "quilt": - provider = &Quilt{ - MinecraftVersion: minecraftVersion, - QuiltVersion: flavorVersion, - } - case "forge": - provider = &Forge{ - MinecraftVersion: minecraftVersion, - ForgeVersion: flavorVersion, - } - case "paper": - provider = &Paper{ - MinecraftVersion: minecraftVersion, - } - } - - if provider == nil { - return nil, errors.New("no provider for this flavor") - } - - return provider, nil -} diff --git a/server/quilt.go b/server/quilt.go index a03cae8..1d74322 100644 --- a/server/quilt.go +++ b/server/quilt.go @@ -12,18 +12,18 @@ import ( const quiltInstallerUrl = "https://maven.quiltmc.org/repository/release/org/quiltmc/quilt-installer/latest/quilt-installer-latest.jar" -type Quilt struct { +type QuiltInstaller struct { MinecraftVersion string QuiltVersion string } -func (provider *Quilt) Provide(serverDir string, serverFile string) error { +func (inst *QuiltInstaller) Install(serverDir string, serverFile string) error { installer, err := requester.DefaultHttpClient.DownloadFile(quiltInstallerUrl, ".", "") if err != nil { return err } - cmd := exec.Command("java", "-jar", installer, "install", "server", provider.MinecraftVersion, "--install-dir="+serverDir, "--create-scripts", "--download-server") + cmd := exec.Command("java", "-jar", installer, "install", "server", inst.MinecraftVersion, "--install-dir="+serverDir, "--create-scripts", "--download-server") fmt.Println("Executing command:", cmd.String()) err = cmd.Run() if err != nil { diff --git a/server/vanilla.go b/server/vanilla.go index 64af1d7..6547747 100644 --- a/server/vanilla.go +++ b/server/vanilla.go @@ -1,19 +1,21 @@ package server import ( + "crypto" "errors" + "github.com/nothub/hashutils/chksum" + "github.com/nothub/hashutils/encoding" "github.com/nothub/mrpack-install/mojang" "github.com/nothub/mrpack-install/requester" - "github.com/nothub/mrpack-install/util" "log" ) -type Vanilla struct { +type VanillaInstaller struct { MinecraftVersion string } -func (provider *Vanilla) Provide(serverDir string, serverFile string) error { - meta, err := mojang.GetMeta(provider.MinecraftVersion) +func (inst *VanillaInstaller) Install(serverDir string, serverFile string) error { + meta, err := mojang.GetMeta(inst.MinecraftVersion) if err != nil { return err } @@ -23,7 +25,7 @@ func (provider *Vanilla) Provide(serverDir string, serverFile string) error { log.Fatalln(err) } - ok, err := util.CheckFileSha1(meta.Downloads.Server.Sha1, file) + ok, err := chksum.VerifyFile(file, meta.Downloads.Server.Sha1, crypto.SHA1.New(), encoding.Hex) if err != nil { log.Fatalln(err) } diff --git a/update/actions.go b/update/actions.go index 2c063ad..2d5a002 100644 --- a/update/actions.go +++ b/update/actions.go @@ -2,7 +2,10 @@ package update import ( "archive/zip" + "crypto" "fmt" + "github.com/nothub/hashutils/chksum" + "github.com/nothub/hashutils/encoding" "github.com/nothub/mrpack-install/requester" "github.com/nothub/mrpack-install/util" "io" @@ -23,7 +26,7 @@ func GetFileStrategy(hash string, path string) Strategy { if !util.PathIsFile(path) { return NoOp } - match, _ := util.CheckFileSha1(hash, path) + match, _ := chksum.VerifyFile(path, hash, crypto.SHA1.New(), encoding.Hex) if match { return Delete } else { @@ -37,7 +40,7 @@ type Actions map[string]Strategy // 1.File does not exist notice // 2.File exists but hash value does not match, change the original file name to xxx.bak // 3.File exists and the hash value matches -func GetDeletionActions(deletions *ModPackInfo, serverPath string) Actions { +func GetDeletionActions(deletions *PackState, serverPath string) Actions { actions := make(Actions, 10) for filePath := range deletions.Hashes { t := GetFileStrategy(deletions.Hashes[filePath], filepath.Join(serverPath, string(filePath))) @@ -57,7 +60,7 @@ func GetDeletionActions(deletions *ModPackInfo, serverPath string) Actions { // 1.File does not exist // 2.File exists but hash value does not match, change the original file name to xxx.bak // 3.File exists and the hash value matches, remove the item from the queue -func GetUpdateActions(updates *ModPackInfo, serverPath string) Actions { +func GetUpdateActions(updates *PackState, serverPath string) Actions { actions := make(Actions, 10) for filePath := range updates.Hashes { switch GetFileStrategy(updates.Hashes[filePath], filepath.Join(serverPath, string(filePath))) { @@ -96,13 +99,17 @@ func ModPackDeleteDo(deleteList Actions, serverPath string) error { return nil } -func ModPackUpdateDo(updateList Actions, updateFileInfo util.Hashes, serverPath string, modPackPath string, downloadPools *requester.DownloadPools) error { +func ModPackUpdateDo(updateList Actions, updateFileInfo map[string]string, serverPath string, modPackPath string, downloadThreads int, retryTimes int) error { + + var downloads []*requester.Download + downloadPools := requester.NewDownloadPools(requester.DefaultHttpClient, downloads, downloadThreads, retryTimes) + // backup file and download file in modrinth index for filePath := range updateList { switch updateList[filePath] { case NoOp: if updateFileInfo[filePath].DownloadLink != nil { - downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath]}, filepath.Base(string(filePath)), filepath.Join(serverPath, filepath.Dir(string(filePath))))) + downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath]}, filepath.Base(filePath), filepath.Join(serverPath, filepath.Dir(filePath)))) } case Backup: err := os.MkdirAll(filepath.Dir(filepath.Join(serverPath, "updateBack", string(filePath))), 0755) @@ -114,7 +121,7 @@ func ModPackUpdateDo(updateList Actions, updateFileInfo util.Hashes, serverPath return err } if updateFileInfo[filePath].DownloadLink != nil { - downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath]}, filepath.Base(string(filePath)), filepath.Join(serverPath, filepath.Dir(string(filePath))))) + downloadPools.Downloads = append(downloadPools.Downloads, requester.NewDownload(updateFileInfo[filePath].DownloadLink, map[string]string{"sha1": updateFileInfo[filePath]}, filepath.Base(filePath), filepath.Join(serverPath, filepath.Dir(filePath)))) } } } diff --git a/update/model.go b/update/model.go index da5c79a..ee5cd0e 100644 --- a/update/model.go +++ b/update/model.go @@ -4,15 +4,14 @@ import ( "bytes" "encoding/json" "github.com/nothub/mrpack-install/modrinth/mrpack" - "github.com/nothub/mrpack-install/util" "os" ) -type ModPackInfo struct { +type PackState struct { Schema uint8 `json:"schema_version"` PackVersion string `json:"pack_version"` PackName string `json:"pack_name"` - Hashes util.Hashes `json:"hashes"` + Hashes map[string]string `json:"hashes"` Dependencies mrpack.Dependencies `json:"dependencies"` } @@ -21,7 +20,7 @@ type FileInfo struct { DownloadLinks []string `json:"download_links"` } -func ReadModPackInfo(modPackJsonFile string) (*ModPackInfo, error) { +func ReadModPackInfo(modPackJsonFile string) (*PackState, error) { var modPackJsonByte []byte var err error @@ -30,7 +29,7 @@ func ReadModPackInfo(modPackJsonFile string) (*ModPackInfo, error) { return nil, err } - modPackJson := ModPackInfo{} + modPackJson := PackState{} err = json.Unmarshal(modPackJsonByte, &modPackJson) if err != nil { @@ -39,7 +38,7 @@ func ReadModPackInfo(modPackJsonFile string) (*ModPackInfo, error) { return &modPackJson, nil } -func (modPackInfo *ModPackInfo) Write(modPackJsonFile string) error { +func (modPackInfo *PackState) Write(modPackJsonFile string) error { if modPackInfo != nil { modPackJsonByte, err := json.Marshal(modPackInfo) var out bytes.Buffer diff --git a/update/packinfo.go b/update/packinfo.go index cd4f6a6..eb0261d 100644 --- a/update/packinfo.go +++ b/update/packinfo.go @@ -2,29 +2,40 @@ package update import ( "archive/zip" + "crypto" "errors" "fmt" + "github.com/nothub/hashutils/chksum" + "github.com/nothub/hashutils/encoding" modrinth "github.com/nothub/mrpack-install/modrinth/api" "github.com/nothub/mrpack-install/modrinth/mrpack" - "github.com/nothub/mrpack-install/util" "reflect" "strings" ) -func GenerateModPackInfo(modrinthIndex *mrpack.Index) (*ModPackInfo, error) { - var modPackInfo ModPackInfo - modPackInfo.Hashes = make(util.Hashes) +func GenerateModPackInfo(zipFile string) (*PackState, error) { + index, err := mrpack.ReadIndex(zipFile) + if err != nil { + return nil, err + } + + var state PackState + state.PackName = index.Name + state.PackVersion = index.VersionId + state.Dependencies = index.Dependencies - modPackInfo.Dependencies = modrinthIndex.Dependencies - modPackInfo.PackVersion = modrinthIndex.VersionId - modPackInfo.PackName = modrinthIndex.Name + hashes, err := mrpack.GetOverrideHashes(zipFile) + if err != nil { + return nil, err + } + state.Hashes = *hashes // Add modrinth.index file - for _, file := range modrinthIndex.Files { + for _, file := range index.Files { if file.Env.Server == modrinth.UnsupportedEnvSupport { continue } - modPackInfo.Hashes[file.Path] = string(file.Hashes.Sha1) + state.Hashes[file.Path] = string(file.Hashes.Sha1) } // Add overrides file @@ -58,7 +69,7 @@ func GenerateModPackInfo(modrinthIndex *mrpack.Index) (*ModPackInfo, error) { return nil, err } - fileHash, err := util.GetReadCloserSha1(readCloser) + fileHash, err := chksum.Create(readCloser, crypto.SHA512.New(), encoding.Hex) if err != nil { return nil, err } @@ -66,13 +77,13 @@ func GenerateModPackInfo(modrinthIndex *mrpack.Index) (*ModPackInfo, error) { if err != nil { return nil, err } - modPackInfo.Hashes[filePath] = fileHash + state.Hashes[filePath] = fileHash } - return &modPackInfo, nil + return &state, nil } -func CompareModPackInfo(old ModPackInfo, new ModPackInfo) (deleteFileInfo *ModPackInfo, updateFileInfo *ModPackInfo, err error) { +func CompareModPackInfo(old PackState, new PackState) (deleteFileInfo *PackState, updateFileInfo *PackState, err error) { if old.PackName != new.PackName || !reflect.DeepEqual(old.Dependencies, new.Dependencies) { return nil, nil, errors.New("for mismatched versions, please upgrade manually") } diff --git a/util/file.go b/util/file.go index a85cbde..dafa41d 100644 --- a/util/file.go +++ b/util/file.go @@ -9,6 +9,8 @@ import ( "strings" ) +var ErrFileIsDir = errors.New("file is a directory") + func PathIsFile(path string) bool { info, err := os.Stat(path) if err != nil { diff --git a/util/hash.go b/util/hash.go deleted file mode 100644 index 790688a..0000000 --- a/util/hash.go +++ /dev/null @@ -1,50 +0,0 @@ -package util - -import ( - "crypto/sha1" - "encoding/hex" - "fmt" - "io" - "log" - "os" -) - -// Hashes maps file paths to hashes. -type Hashes map[string]string - -func Sha1(data *[]byte) string { - s := sha1.New() - s.Write(*data) - return hex.EncodeToString(s.Sum(nil)) -} - -func GetReadCloserSha1(readCloser io.ReadCloser) (string, error) { - verifyByte, err := io.ReadAll(readCloser) - if err != nil { - return "", err - } - return Sha1(&verifyByte), nil -} - -func CheckFileSha1(verifyHash string, verifyFile string) (bool, error) { - _, err := os.Stat(verifyFile) - if err != nil { - log.Fatalln("The validated file does not exist", verifyFile) - } - file, err := os.Open(verifyFile) - defer func(file *os.File) { - err := file.Close() - if err != nil { - fmt.Println(err) - } - }(file) - - if err != nil { - return false, err - } - newFileHash, err := GetReadCloserSha1(file) - if err != nil { - return false, err - } - return verifyHash == newFileHash, nil -}