diff --git a/.gitignore b/.gitignore index 8709883..91f3930 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,4 @@ ### Personal Development Script and Environment ### personal-build-and-develop.* .env +test.json diff --git a/README.md b/README.md index e676740..0f9cee8 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ -# ForgeCLI +# ForgeCLI Guide Package was created with the express intent to remove the guess work out of Mod acquiring and updating. -## Getting Your Forge API Key +## Getting Your Forge API Key: This is more complicated because you will be pulling/using the latest mod for the release of your game. To get started make sure you have a [CursedForge API Key](https://docs.curseforge.com/#getting-started). Then use it as a parameter for your build -## Quick start +## Basic Run: Simple command to download latest fabric modules: @@ -14,7 +14,28 @@ Simple command to download latest fabric modules: . ./forgecli.exe -forgekey '$2a$10...' -projects "416089,391366,552655" -family "fabric" -debug ``` -## ForgeCLI Usage Details +## Example Outputs: + +**Example Successful Run** + +```bash +time="2022-02-28T09:37:51-08:00" level=info msg="Starting Forge Mod lookup" +time="2022-02-28T09:37:51-08:00" level=info msg="Found Lastest FileID: 3667363 for Mod: 416089" +time="2022-02-28T09:37:51-08:00" level=info msg="Downloading: https://edge.forgecdn.net/files/3667/363/voicechat-fabric-1.18.2-2.2.24.jar" +time="2022-02-28T09:37:52-08:00" level=info msg="Files in Destination Folder:" +time="2022-02-28T09:37:52-08:00" level=info msg=" voicechat-fabric-1.18.2-2.2.24.jar " +time="2022-02-28T09:37:52-08:00" level=info msg="Download Complete." +``` + +**Example Failed Run** + +```bash +time="2022-02-28T09:52:14-08:00" level=info msg="Starting Forge Mod Lookup" +time="2022-02-28T09:52:14-08:00" level=error msg="could not find 391366 for minecraft version: 1.18.2 or family: fabric" +time="2022-02-28T09:52:14-08:00" level=error msg=Exiting... +``` + +## ForgeCLI Usage: **NOTE:** Due to the lack of Version pinning this can lead to unexpected behavior if the publisher updates mod unexpectedly. @@ -26,7 +47,7 @@ To get started make sure you have a [CursedForge API Key](https://docs.curseforg - `Mac:` "~/Library/Application Support/minecraft/mods" - `Linux:` "~/Library/Application Support/minecraft/mods" -**Mod basics** +**Mod Basics** - `Mod Release types:` Release, Beta, and Alpha. - `Mod dependencies:` Required and Optional @@ -42,8 +63,8 @@ To get started make sure you have a [CursedForge API Key](https://docs.curseforg - `family:` Used to filter mods based on server type. Options are Forge, Fabric, and Bukkit - `release:` Default is Release, Used to allow for Beta and Alpha mod downloads. - `version:` Default is LATEST, but this is Minecraft VERSION. e.g. 1.18.2 -- `clearMods:` Default is false, allows CLI to remove all mods before downloading new Mods. -- `downloadDependencies:` Default is True, this uses the mods required dependencies to download missing mods. +- `clear:` Default is false, allows CLI to remove all mods before downloading new Mods. +- `dependencies:` Default is True, this uses the mods required dependencies to download missing mods. - `debug:` Enable extra logging. ## JSON File usage: @@ -56,7 +77,6 @@ To get started make sure you have a [CursedForge API Key](https://docs.curseforg **Field Description** - - `name:` is currently unused, but can be used to document each entry. - `projectID:` is the id found on the CurseForge website for a particular mod - `releaseType:` Type corresponds to forge's R, B, A icon for each file. Default Release, options are (release|beta|alpha). @@ -79,13 +99,13 @@ To get started make sure you have a [CursedForge API Key](https://docs.curseforg { "name": "Biomes o plenty", "projectID": "220318", - "fileName": "BiomesOPlenty-1.18.1-15.0.0.100-universal.jar", + "fileName": "BiomesOPlenty-1.18.2-15.0.0.100-universal.jar", "releaseType": "release" } ] ``` -### Manually Building and Testing +## Manually Building and Testing: Make a `./.env` file in the root folder and add your forge key. @@ -106,8 +126,6 @@ go build # . ./forgecli should now be available to be used. ``` -### TODO List +## TODO List: -- Update and proof read documentation -- Add normal info logging. Currently the app runs silent without -debug -- add fileName filter from json mods. Currently we have no method to pin versions +- add fileName filter from json mods. Currently we have no method to pin versions. diff --git a/forgecli/forgeapi.go b/forgecli/forgeapi.go index 1e5e75d..75fbbe0 100644 --- a/forgecli/forgeapi.go +++ b/forgecli/forgeapi.go @@ -28,6 +28,25 @@ var releaseLookup = map[string]ReleaseType{ "alpha": Alpha, } +// FamilyType string declaration +type FamilyType string + +const ( + // Fabric used for validating we are using the correct family types + Fabric FamilyType = "fabric" + // Bukkit used for validating we are using the correct family types + Bukkit FamilyType = "bukkit" + // Forge used for validating we are using the correct family types + Forge FamilyType = "forge" +) + +// familyTypeLookup used when accepting strings and converting to ints +var familyTypeLookup = map[string]FamilyType{ + "fabric": Fabric, + "bukkit": Bukkit, + "forge": Forge, +} + // ForgePagination was to be used with Pagination, currently not used type ForgePagination struct { Index int `json:"index"` diff --git a/forgecli/forgecli.go b/forgecli/forgecli.go index d07f3f2..39f0575 100644 --- a/forgecli/forgecli.go +++ b/forgecli/forgecli.go @@ -33,7 +33,7 @@ type appEnv struct { projectIDs string downloadDependencies bool clearMods bool - modfamily string + modfamily FamilyType modReleaseType ReleaseType destination string modsFromJSON JSONMods @@ -55,20 +55,27 @@ func (app *appEnv) fromArgs(args []string) error { fl.BoolVar(&app.downloadDependencies, "dependencies", true, "Download Mods Dependencies") fl.BoolVar(&app.clearMods, "clear", false, "Clear Mods from destination (mods folder)") fl.BoolVar(&app.isDebug, "debug", false, "enable debug logrusging") - fl.StringVar(&app.modfamily, "family", "", "Minecraft type: Vanilla, Fabric, Forge, Bukkit") fl.StringVar(&app.projectIDs, "projects", "", "Forge Project IDs separated by commas 12345,67890") inputReleaseType := fl.String("release", "release", "Mods release type, release, beta, alpha") - app.modReleaseType = releaseLookup[*inputReleaseType] + inputFamily := fl.String("family", "", "Minecraft type: Vanilla, Fabric, Forge, Bukkit") + // Parsing the Args before they can be used if err := fl.Parse(args); err != nil { return err } + // Type Conversions + app.modReleaseType = releaseLookup[*inputReleaseType] + if len(*inputFamily) > 0 { + app.modfamily = familyTypeLookup[*inputFamily] + } + // Setting up logrus if app.isDebug { logrus.SetLevel(logrus.DebugLevel) } + // Checking for Required Fields if app.projectIDs == "" && app.jsonFile == "" { fmt.Fprintf(os.Stderr, "Did not receive Project IDs to Download.\n") fl.Usage() @@ -79,7 +86,7 @@ func (app *appEnv) fromArgs(args []string) error { } func (app *appEnv) run() error { - logrus.Debug("Starting Run") + logrus.Info("Starting Forge Mod Lookup") app.SetForgeAPIKey() app.GetMCVersion() @@ -96,7 +103,8 @@ func (app *appEnv) run() error { app.PrepareDestinationFolder() app.DownloadMods() - logrus.Debug("Ending Run") + app.PrintDestinationFiles() + logrus.Info("Download Complete.") return nil } @@ -125,7 +133,8 @@ func (app *appEnv) GetModsByProjectIDs() { projectIDs := strings.Split(app.projectIDs, ",") for _, projectID := range projectIDs { logrus.Debugf("Getting Mod: %s", projectID) - app.GetModsFromForge(projectID, app.modReleaseType) + err := app.GetModsFromForge(projectID, app.modReleaseType) + check(err) } } @@ -140,9 +149,10 @@ func (app *appEnv) GetModsByJSONFile() { if fileMod.ReleaseType != "" { releaseType = releaseLookup[fileMod.ReleaseType] } else { - releaseType = releaseLookup["release"] + releaseType = app.modReleaseType } - app.GetModsFromForge(fileMod.ProjectID, releaseType) + err := app.GetModsFromForge(fileMod.ProjectID, releaseType) + check(err) } } @@ -154,14 +164,15 @@ func (app *appEnv) GetModsDependencies() error { for _, modDep := range mod.Dependencies { if modDep.RelationType == 3 { logrus.Debugf("Getting Mod dependency: %s", strconv.Itoa(modDep.ModID)) - app.GetModsFromForge(strconv.Itoa(modDep.ModID), releaseLookup["release"]) + err := app.GetModsFromForge(strconv.Itoa(modDep.ModID), releaseLookup["release"]) + check(err) } } } return nil } -func (app *appEnv) GetModsFromForge(modID string, releaseType ReleaseType) { +func (app *appEnv) GetModsFromForge(modID string, releaseType ReleaseType) error { var resp ForgeMods pageIndex := 0 pageSize := 999 @@ -169,7 +180,7 @@ func (app *appEnv) GetModsFromForge(modID string, releaseType ReleaseType) { "https://api.curseforge.com/v1/mods/%s/files?gameVersionTypeID=%d&index=%d&pageSize=%d", modID, app.forgeGameVersionType, pageIndex, pageSize, ) - err := app.fetchforgeAPIJSON(url, &resp) + err := app.FetchForgeAPIJSON(url, &resp) check(err) foundID := 0 @@ -180,8 +191,12 @@ func (app *appEnv) GetModsFromForge(modID string, releaseType ReleaseType) { foundMod = currMod } } + if foundID == 0 { + return fmt.Errorf("could not find %s for minecraft version: %s or family: %s", modID, app.version, app.modfamily) + } app.modsToDownload[foundID] = foundMod - logrus.Debugf("Found Lastest FileID: %d for Mod: %s", foundID, modID) + logrus.Infof("Found Lastest FileID: %d for Mod: %s", foundID, modID) + return nil } func (app *appEnv) ModFilter(currMod ForgeMod) bool { @@ -199,7 +214,7 @@ func (app *appEnv) ModFilter(currMod ForgeMod) bool { func (app *appEnv) GetMCVersion() error { var resp MCVersionResponse - if err := app.fetchJSON(MinecraftVersionURL, &resp); err != nil { + if err := app.FetchJSON(MinecraftVersionURL, &resp); err != nil { return fmt.Errorf("could not get minecraft version from:\n%s", MinecraftVersionURL) } if app.version == "" { @@ -223,15 +238,16 @@ func (app *appEnv) GetVersionTypeNumber() error { // Forge has a specific format to validate Minecraft 1.17 shortNumber := strings.Join(strings.Split(app.version, ".")[:2], ".") forgeVersionName := "Minecraft " + shortNumber + logrus.Debugf("Fetching VersionType for MC version: %s, %s", forgeVersionName, ForgeVersionTypeURL) var resp ForgeVersions - if err := app.fetchforgeAPIJSON(ForgeVersionTypeURL, &resp); err != nil { + if err := app.FetchForgeAPIJSON(ForgeVersionTypeURL, &resp); err != nil { return err } for _, v := range resp.Data { if v.Name == forgeVersionName { - returnGameVersionType := v.ID - app.forgeGameVersionType = returnGameVersionType + app.forgeGameVersionType = v.ID + logrus.Debugf("found VersionType: %d", v.ID) return nil } } @@ -241,7 +257,7 @@ func (app *appEnv) GetVersionTypeNumber() error { func (app *appEnv) DownloadMods() error { for _, mod := range app.modsToDownload { - if err := app.fetchAndSave(mod.DownloadURL, mod.Filename); err != nil { + if err := app.FetchAndSave(mod.DownloadURL, mod.Filename); err != nil { return err } } diff --git a/forgecli/forgecli_test.go b/forgecli/forgecli_test.go index fd9ed8b..dabeb47 100644 --- a/forgecli/forgecli_test.go +++ b/forgecli/forgecli_test.go @@ -7,9 +7,11 @@ import ( "testing" "github.com/joho/godotenv" + "github.com/sirupsen/logrus" ) func LoadDotEnv() { + logrus.SetLevel(logrus.DebugLevel) if os.Getenv("FORGEKEY") == "" { err := godotenv.Load("../.env") check(err) @@ -25,6 +27,7 @@ func TestCLIReturnsError(t *testing.T) { } } +// Test will fail as MC receives version updates func TestGetVersionTypeNumber(t *testing.T) { LoadDotEnv() var app appEnv @@ -83,7 +86,7 @@ func TestFetchforgeAPIJSON(t *testing.T) { app.forgeKey = os.Getenv("FORGEKEY") var resp ForgeMods url := "https://api.curseforge.com/v1/mods/306612/files?gameVersionTypeID=73250&index=0&pageSize=3" - if err := app.fetchforgeAPIJSON(url, &resp); err != nil { + if err := app.FetchForgeAPIJSON(url, &resp); err != nil { t.Errorf("Test failed, by throwing error") } fmt.Println(resp.Data[0].DisplayName) @@ -93,7 +96,7 @@ func TestGetMCVersionNoInput(t *testing.T) { var app appEnv app.hc = *http.DefaultClient app.version = "" - expected := "1.18.1" + expected := "1.18.2" app.GetMCVersion() if app.version != expected { t.Errorf("Test failed, expected: '%s', got: '%s'", expected, app.version) diff --git a/forgecli/helpers.go b/forgecli/helpers.go index 9a63139..9454bf8 100644 --- a/forgecli/helpers.go +++ b/forgecli/helpers.go @@ -15,9 +15,15 @@ import ( "github.com/sirupsen/logrus" ) +type error interface { + Error() string +} + func check(e error) { if e != nil { - panic(e) + logrus.Error(e.Error()) + logrus.Error("Exiting...") + os.Exit(1) } } @@ -30,7 +36,7 @@ func contains(s []string, e string) bool { return false } -func (app *appEnv) osTargetDirectory() { +func (app *appEnv) GetTargetDirectory() { if app.destination != "" { return } @@ -50,7 +56,7 @@ func (app *appEnv) osTargetDirectory() { } } -func (app *appEnv) ensureDestination() { +func (app *appEnv) EnsureDestination() { logrus.Debugf("Making Folder if not exist: %s", app.destination) err := os.MkdirAll(app.destination, os.ModeDir) if err != nil && !os.IsExist(err) { @@ -59,17 +65,17 @@ func (app *appEnv) ensureDestination() { } func (app *appEnv) PrepareDestinationFolder() { - app.osTargetDirectory() + app.GetTargetDirectory() logrus.Debugf("Mod Destination is set to: %s", app.destination) if app.clearMods { logrus.Debugf("Removing contents of: %s", app.destination) err := os.RemoveAll(app.destination) check(err) } - app.ensureDestination() + app.EnsureDestination() } -func (app *appEnv) fetchforgeAPIJSON(url string, data interface{}) error { +func (app *appEnv) FetchForgeAPIJSON(url string, data interface{}) error { logrus.Debugf("Fetching: %s", url) req, err := http.NewRequest("GET", url, nil) check(err) @@ -83,7 +89,7 @@ func (app *appEnv) fetchforgeAPIJSON(url string, data interface{}) error { return json.NewDecoder(resp.Body).Decode(data) } -func (app *appEnv) fetchJSON(url string, data interface{}) error { +func (app *appEnv) FetchJSON(url string, data interface{}) error { logrus.Debugf("Fetching JSON: %s", url) resp, err := app.hc.Get(url) check(err) @@ -106,9 +112,16 @@ func (app *appEnv) LoadModsFromJSON() { app.modsFromJSON = result } -func (app *appEnv) fetchAndSave(url, destPath string) error { - logrus.Debugf("Fetching and Saving: %s", url) - resp, err := app.hc.Get(url) +func (app *appEnv) FetchAndSave(url, destPath string) error { + logrus.Infof("Downloading: %s", url) + + req, err := http.NewRequest("GET", url, nil) + check(err) + req.Header = http.Header{ + "Accept": []string{"application/json"}, + "x-api-key": []string{app.forgeKey}, + } + resp, err := app.hc.Do(req) check(err) defer resp.Body.Close() @@ -118,3 +131,15 @@ func (app *appEnv) fetchAndSave(url, destPath string) error { _, err = io.Copy(f, resp.Body) return err } + +func (app *appEnv) PrintDestinationFiles() { + files, err := ioutil.ReadDir(app.destination) + if err != nil { + log.Fatal(err) + } + + logrus.Infof("Files in Destination Folder:") + for _, file := range files { + logrus.Infof(" %s ", file.Name()) + } +} diff --git a/forgecli/helpers_test.go b/forgecli/helpers_test.go index 1eee389..8148073 100644 --- a/forgecli/helpers_test.go +++ b/forgecli/helpers_test.go @@ -6,7 +6,7 @@ import ( func TestOSTargetDirectory(t *testing.T) { var app appEnv - app.osTargetDirectory() + app.GetTargetDirectory() expected := "mods" if app.destination[len(app.destination)-4:] != expected { t.Errorf("Test failed, expected: '%s', got: '%s'", expected, app.destination) @@ -16,7 +16,7 @@ func TestOSTargetDirectory(t *testing.T) { // func TestEnsureDestination(t *testing.T) { // var app appEnv // app.destination = "folder/test" -// app.ensureDestination() +// app.EnsureDestination() // if _, err := os.Stat(app.destination); os.IsNotExist(err) { // t.Errorf("Test failed, expected to create: '%s'", app.destination) // }