mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-20 21:36:30 +02:00
github: fixes and improvements
- "file" -> "asset" - "version" -> "tag" or "release" (where appropriate) - fix updater.go for upstream changes - make printed log messages more similar to those of other modules - move http request function(s) to separate file "request.go" - remove the concept of a "Mod"; we're using "Repo"s (GitHub repositories) instead - remove unnecessary fields in structs - use sha256 instead of sha1 for asset checksums Signed-off-by: unilock <unilock@fennet.rentals>
This commit is contained in:
parent
837b4db760
commit
6116393310
133
github/github.go
133
github/github.go
@ -4,9 +4,8 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
"github.com/packwiz/packwiz/cmd"
|
||||
@ -27,15 +26,15 @@ func init() {
|
||||
|
||||
func fetchRepo(slug string) (Repo, error) {
|
||||
var repo Repo
|
||||
res, err := http.Get(githubApiUrl + "repos/" + slug)
|
||||
|
||||
res, err := http.Get(githubApiUrl + "repos/" + slug)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
|
||||
defer res.Body.Close()
|
||||
|
||||
repoBody, err := ioutil.ReadAll(res.Body)
|
||||
repoBody, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
@ -44,49 +43,24 @@ func fetchRepo(slug string) (Repo, error) {
|
||||
if err != nil {
|
||||
return repo, err
|
||||
}
|
||||
|
||||
if repo.FullName == "" {
|
||||
return repo, errors.New("invalid json while fetching mod: " + slug)
|
||||
}
|
||||
|
||||
return repo, nil
|
||||
}
|
||||
|
||||
func fetchMod(slug string) (Mod, error) {
|
||||
|
||||
var mod Mod
|
||||
|
||||
repo, err := fetchRepo(slug)
|
||||
|
||||
if err != nil {
|
||||
return mod, err
|
||||
type Repo struct {
|
||||
ID int `json:"id"`
|
||||
NodeID string `json:"node_id"` // TODO: use this with GH API, instead of name (to acct. for repo renames?) + store in mod.pw.toml
|
||||
Name string `json:"name"` // "hello_world"
|
||||
FullName string `json:"full_name"` // "owner/hello_world"
|
||||
}
|
||||
|
||||
release, err := getLatestVersion(slug, "")
|
||||
|
||||
if err != nil {
|
||||
return mod, err
|
||||
}
|
||||
|
||||
mod = Mod{
|
||||
ID: repo.Name,
|
||||
Slug: slug,
|
||||
Team: repo.Owner.Login,
|
||||
Title: repo.Name,
|
||||
Description: repo.Description,
|
||||
Published: repo.CreatedAt,
|
||||
Updated: release.CreatedAt,
|
||||
License: repo.License,
|
||||
ClientSide: "unknown",
|
||||
ServerSide: "unknown",
|
||||
Categories: repo.Topics,
|
||||
}
|
||||
if mod.ID == "" {
|
||||
return mod, errors.New("invalid json whilst fetching mod: " + slug)
|
||||
}
|
||||
|
||||
return mod, nil
|
||||
|
||||
}
|
||||
|
||||
type ModReleases struct {
|
||||
type Release struct {
|
||||
URL string `json:"url"`
|
||||
NodeID string `json:"node_id"`
|
||||
NodeID string `json:"node_id"` // TODO: probably also use this with GH API
|
||||
TagName string `json:"tag_name"`
|
||||
TargetCommitish string `json:"target_commitish"` // The branch of the release
|
||||
Name string `json:"name"`
|
||||
@ -96,29 +70,8 @@ type ModReleases struct {
|
||||
|
||||
type Asset struct {
|
||||
URL string `json:"url"`
|
||||
Name string `json:"name"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
BrowserDownloadURL string `json:"browser_download_url"`
|
||||
}
|
||||
|
||||
type Repo struct {
|
||||
ID int `json:"id"`
|
||||
Name string `json:"name"`
|
||||
FullName string `json:"full_name"`
|
||||
Owner struct {
|
||||
Login string `json:"login"`
|
||||
} `json:"owner"`
|
||||
Description string `json:"description"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
License struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
SpdxID string `json:"spdx_id"`
|
||||
URL string `json:"url"`
|
||||
NodeID string `json:"node_id"`
|
||||
} `json:"license"`
|
||||
Topics []string `json:"topics"`
|
||||
}
|
||||
|
||||
func (u ghUpdateData) ToMap() (map[string]interface{}, error) {
|
||||
@ -127,58 +80,28 @@ func (u ghUpdateData) ToMap() (map[string]interface{}, error) {
|
||||
return newMap, err
|
||||
}
|
||||
|
||||
type License struct {
|
||||
Id string `json:"id"` //The license id of a mod, retrieved from the licenses get route
|
||||
Name string `json:"name"` //The long for name of a license
|
||||
Url string `json:"url"` //The URL to this license
|
||||
}
|
||||
|
||||
type Mod struct {
|
||||
ID string `json:"id"` //The ID of the mod, encoded as a base62 string
|
||||
Slug string `json:"slug"` //The slug of a mod, used for vanity URLs
|
||||
Team string `json:"team"` //The id of the team that has ownership of this mod
|
||||
Title string `json:"title"` //The title or name of the mod
|
||||
Description string `json:"description"` //A short description of the mod
|
||||
// Body string `json:"body"` //A long form description of the mod.
|
||||
// BodyUrl string `json:"body_url"` //DEPRECATED The link to the long description of the mod (Optional)
|
||||
Published string `json:"published"` //The date at which the mod was first published
|
||||
Updated string `json:"updated"` //The date at which the mod was updated
|
||||
License struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
SpdxID string `json:"spdx_id"`
|
||||
URL string `json:"url"`
|
||||
NodeID string `json:"node_id"`
|
||||
} `json:"license"`
|
||||
ClientSide string `json:"client_side"` //The support range for the client mod - required, optional, unsupported, or unknown
|
||||
ServerSide string `json:"server_side"` //The support range for the server mod - required, optional, unsupported, or unknown
|
||||
// Downloads int `json:"downloads"` //The total number of downloads the mod has
|
||||
Categories []string `json:"categories"` //A list of the categories that the mod is in
|
||||
Versions []string `json:"versions"` //A list of ids for versions of the mod
|
||||
IconUrl string `json:"icon_url"` //The URL of the icon of the mod (Optional)
|
||||
IssuesUrl string `json:"issues_url"` //An optional link to where to submit bugs or issues with the mod (Optional)
|
||||
SourceUrl string `json:"source_url"` //An optional link to the source code for the mod (Optional)
|
||||
WikiUrl string `json:"wiki_url"` //An optional link to the mod's wiki page or other relevant information (Optional)
|
||||
DiscordUrl string `json:"discord_url"` //An optional link to the mod's discord (Optional)
|
||||
}
|
||||
|
||||
func (u Asset) getSha1() (string, error) {
|
||||
func (u Asset) getSha256() (string, error) {
|
||||
// TODO potentionally cache downloads to speed things up and avoid getting ratelimited by github!
|
||||
mainHasher, err := core.GetHashImpl("sha1")
|
||||
mainHasher, err := core.GetHashImpl("sha256")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
resp, err := http.Get(u.BrowserDownloadURL)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode == 404 {
|
||||
return "", fmt.Errorf("Asset not found")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return "", fmt.Errorf("Invalid response code: %d", resp.StatusCode)
|
||||
return "", fmt.Errorf("invalid response status: %v", resp.StatusCode)
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
mainHasher.Write(body)
|
||||
|
||||
hash := mainHasher.Sum(nil)
|
||||
|
@ -4,8 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
@ -16,48 +15,46 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
var GithubRegex = regexp.MustCompile("https?://(?:www\\.)?github\\.com/([^/]+/[^/]+)")
|
||||
var GithubRegex = regexp.MustCompile(`^https?://(?:www\.)?github\.com/([^/]+/[^/]+)`)
|
||||
|
||||
// installCmd represents the install command
|
||||
var installCmd = &cobra.Command{
|
||||
Use: "install [mod]",
|
||||
Short: "Install mods from github releases",
|
||||
Aliases: []string{"add", "get"},
|
||||
Use: "add [URL]",
|
||||
Short: "Add a project from a GitHub repository URL",
|
||||
Aliases: []string{"install", "get"},
|
||||
Args: cobra.ArbitraryArgs,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
pack, err := core.LoadPack()
|
||||
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if len(args) == 0 || len(args[0]) == 0 {
|
||||
fmt.Println("You must specify a mod.")
|
||||
fmt.Println("You must specify a GitHub repository URL.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
//Try interpreting the arg as a modId or slug.
|
||||
//Modrinth transparently handles slugs/mod ids in their api; we don't have to detect which one it is.
|
||||
// Try interpreting the argument as a slug, or GitHub repository URL.
|
||||
var slug string
|
||||
|
||||
//Try to see if it's a site, if extract the id/slug from the url.
|
||||
//Otherwise, interpret the arg as a id/slug straight up
|
||||
// Check if the argument is a valid GitHub repository URL; if so, extract the slug from the URL.
|
||||
// Otherwise, interpret the argument as a slug directly.
|
||||
matches := GithubRegex.FindStringSubmatch(args[0])
|
||||
if matches != nil && len(matches) == 2 {
|
||||
if len(matches) == 2 {
|
||||
slug = matches[1]
|
||||
} else {
|
||||
slug = args[0]
|
||||
}
|
||||
|
||||
mod, err := fetchMod(slug)
|
||||
repo, err := fetchRepo(slug)
|
||||
|
||||
if err != nil {
|
||||
fmt.Println("Failed to get the mod ", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
installMod(mod, pack)
|
||||
installMod(repo, pack)
|
||||
},
|
||||
}
|
||||
|
||||
@ -67,10 +64,8 @@ func init() {
|
||||
|
||||
const githubApiUrl = "https://api.github.com/"
|
||||
|
||||
func installMod(mod Mod, pack core.Pack) error {
|
||||
fmt.Printf("Found repo %s: '%s'.\n", mod.Slug, mod.Description)
|
||||
|
||||
latestVersion, err := getLatestVersion(mod.Slug, "")
|
||||
func installMod(repo Repo, pack core.Pack) error {
|
||||
latestVersion, err := getLatestVersion(repo.FullName, "")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get latest version: %v", err)
|
||||
}
|
||||
@ -78,28 +73,20 @@ func installMod(mod Mod, pack core.Pack) error {
|
||||
return errors.New("mod is not available for this Minecraft version (use the acceptable-game-versions option to accept more) or mod loader")
|
||||
}
|
||||
|
||||
return installVersion(mod, latestVersion, pack)
|
||||
return installVersion(repo, latestVersion, pack)
|
||||
}
|
||||
|
||||
func getLatestVersion(slug string, branch string) (ModReleases, error) {
|
||||
var modReleases []ModReleases
|
||||
var release ModReleases
|
||||
func getLatestVersion(slug string, branch string) (Release, error) {
|
||||
var modReleases []Release
|
||||
var release Release
|
||||
|
||||
resp, err := http.Get(githubApiUrl + "repos/" + slug + "/releases")
|
||||
resp, err := ghDefaultClient.makeGet(slug)
|
||||
if err != nil {
|
||||
return release, err
|
||||
}
|
||||
|
||||
if resp.StatusCode == 404 {
|
||||
return release, fmt.Errorf("mod not found (for URL %v)", githubApiUrl+"repos/"+slug+"/releases")
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return release, fmt.Errorf("invalid response status %v for URL %v", resp.Status, githubApiUrl+"repos/"+slug+"/releases")
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return release, err
|
||||
}
|
||||
@ -116,23 +103,23 @@ func getLatestVersion(slug string, branch string) (ModReleases, error) {
|
||||
return modReleases[0], nil
|
||||
}
|
||||
|
||||
func installVersion(mod Mod, version ModReleases, pack core.Pack) error {
|
||||
var files = version.Assets
|
||||
func installVersion(repo Repo, release Release, pack core.Pack) error {
|
||||
var files = release.Assets
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.New("version doesn't have any files attached")
|
||||
return errors.New("release doesn't have any files attached")
|
||||
}
|
||||
|
||||
// TODO: add some way to allow users to pick which file to install?
|
||||
var file = files[0]
|
||||
for _, v := range version.Assets {
|
||||
for _, v := range release.Assets {
|
||||
if strings.HasSuffix(v.Name, ".jar") {
|
||||
file = v
|
||||
}
|
||||
}
|
||||
|
||||
//Install the file
|
||||
fmt.Printf("Installing %s from version %s\n", file.URL, version.Name)
|
||||
fmt.Printf("Installing %s from release %s\n", file.Name, release.TagName)
|
||||
index, err := pack.LoadIndex()
|
||||
if err != nil {
|
||||
return err
|
||||
@ -141,26 +128,26 @@ func installVersion(mod Mod, version ModReleases, pack core.Pack) error {
|
||||
updateMap := make(map[string]map[string]interface{})
|
||||
|
||||
updateMap["github"], err = ghUpdateData{
|
||||
Slug: mod.Slug,
|
||||
InstalledVersion: version.TagName,
|
||||
Branch: version.TargetCommitish,
|
||||
Slug: repo.FullName,
|
||||
Tag: release.TagName,
|
||||
Branch: release.TargetCommitish,
|
||||
}.ToMap()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hash, error := file.getSha1()
|
||||
if error != nil || hash == "" {
|
||||
return errors.New("file doesn't have a hash")
|
||||
hash, err := file.getSha256()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
modMeta := core.Mod{
|
||||
Name: mod.Title,
|
||||
Name: repo.Name,
|
||||
FileName: file.Name,
|
||||
Side: "unknown",
|
||||
Side: core.UniversalSide,
|
||||
Download: core.ModDownload{
|
||||
URL: file.BrowserDownloadURL,
|
||||
HashFormat: "sha1",
|
||||
HashFormat: "sha256",
|
||||
Hash: hash,
|
||||
},
|
||||
Update: updateMap,
|
||||
@ -170,7 +157,7 @@ func installVersion(mod Mod, version ModReleases, pack core.Pack) error {
|
||||
if folder == "" {
|
||||
folder = "mods"
|
||||
}
|
||||
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, mod.Title+core.MetaExtension))
|
||||
path = modMeta.SetMetaPath(filepath.Join(viper.GetString("meta-folder-base"), folder, repo.Name+core.MetaExtension))
|
||||
|
||||
// If the file already exists, this will overwrite it!!!
|
||||
// TODO: Should this be improved?
|
||||
|
33
github/request.go
Normal file
33
github/request.go
Normal file
@ -0,0 +1,33 @@
|
||||
package github
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const ghApiServer = "api.github.com"
|
||||
|
||||
type ghApiClient struct {
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
var ghDefaultClient = ghApiClient{&http.Client{}}
|
||||
|
||||
func (c *ghApiClient) makeGet(slug string) (*http.Response, error) {
|
||||
req, err := http.NewRequest("GET", "https://" + ghApiServer + "/repos/" + slug + "/releases", nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req.Header.Set("Accept", "application/vnd.github+json")
|
||||
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != 200 {
|
||||
return nil, fmt.Errorf("invalid response status: %v", resp.Status)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
@ -11,7 +11,7 @@ import (
|
||||
|
||||
type ghUpdateData struct {
|
||||
Slug string `mapstructure:"slug"`
|
||||
InstalledVersion string `mapstructure:"version"`
|
||||
Tag string `mapstructure:"tag"`
|
||||
Branch string `mapstructure:"branch"`
|
||||
}
|
||||
|
||||
@ -25,16 +25,16 @@ func (u ghUpdater) ParseUpdate(updateUnparsed map[string]interface{}) (interface
|
||||
|
||||
type cachedStateStore struct {
|
||||
ModID string
|
||||
Version ModReleases
|
||||
Version Release
|
||||
}
|
||||
|
||||
func (u ghUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack) ([]core.UpdateCheck, error) {
|
||||
func (u ghUpdater) CheckUpdate(mods []*core.Mod, pack core.Pack) ([]core.UpdateCheck, error) {
|
||||
results := make([]core.UpdateCheck, len(mods))
|
||||
|
||||
for i, mod := range mods {
|
||||
rawData, ok := mod.GetParsedUpdateData("github")
|
||||
if !ok {
|
||||
results[i] = core.UpdateCheck{Error: errors.New("couldn't parse mod data")}
|
||||
results[i] = core.UpdateCheck{Error: errors.New("failed to parse update metadata")}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -46,13 +46,13 @@ func (u ghUpdater) CheckUpdate(mods []core.Mod, mcVersion string, pack core.Pack
|
||||
continue
|
||||
}
|
||||
|
||||
if newVersion.TagName == data.InstalledVersion { //The latest version from the site is the same as the installed one
|
||||
if newVersion.TagName == data.Tag { // The latest version from the site is the same as the installed one
|
||||
results[i] = core.UpdateCheck{UpdateAvailable: false}
|
||||
continue
|
||||
}
|
||||
|
||||
if len(newVersion.Assets) == 0 {
|
||||
results[i] = core.UpdateCheck{Error: errors.New("new version doesn't have any files")}
|
||||
results[i] = core.UpdateCheck{Error: errors.New("new version doesn't have any assets")}
|
||||
continue
|
||||
}
|
||||
|
||||
@ -80,18 +80,18 @@ func (u ghUpdater) DoUpdate(mods []*core.Mod, cachedState []interface{}) error {
|
||||
}
|
||||
}
|
||||
|
||||
hash, error := file.getSha1()
|
||||
if error != nil || hash == "" {
|
||||
return errors.New("file doesn't have a hash")
|
||||
hash, err := file.getSha256()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
mod.FileName = file.Name
|
||||
mod.Download = core.ModDownload{
|
||||
URL: file.BrowserDownloadURL,
|
||||
HashFormat: "sha1",
|
||||
HashFormat: "sha256",
|
||||
Hash: hash,
|
||||
}
|
||||
mod.Update["github"]["version"] = version.TagName
|
||||
mod.Update["github"]["tag"] = version.TagName
|
||||
}
|
||||
|
||||
return nil
|
||||
|
Loading…
x
Reference in New Issue
Block a user