mirror of
https://github.com/packwiz/packwiz.git
synced 2025-05-04 19:46:30 +02:00
Implement Modrinth pack exporting (fixes #34)
This commit is contained in:
parent
4abf3340a1
commit
60c08b93f3
37
core/hash.go
37
core/hash.go
@ -5,23 +5,44 @@ import (
|
|||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"errors"
|
"encoding/binary"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"github.com/packwiz/packwiz/curseforge/murmur2"
|
||||||
"hash"
|
"hash"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GetHashImpl gets an implementation of hash.Hash for the given hash type string
|
// GetHashImpl gets an implementation of hash.Hash for the given hash type string
|
||||||
func GetHashImpl(hashType string) (hash.Hash, error) {
|
func GetHashImpl(hashType string) (hash.Hash, HashStringer, error) {
|
||||||
switch strings.ToLower(hashType) {
|
switch strings.ToLower(hashType) {
|
||||||
case "sha1":
|
case "sha1":
|
||||||
return sha1.New(), nil
|
return sha1.New(), hexStringer{}, nil
|
||||||
case "sha256":
|
case "sha256":
|
||||||
return sha256.New(), nil
|
return sha256.New(), hexStringer{}, nil
|
||||||
case "sha512":
|
case "sha512":
|
||||||
return sha512.New(), nil
|
return sha512.New(), hexStringer{}, nil
|
||||||
case "md5":
|
case "md5":
|
||||||
return md5.New(), nil
|
return md5.New(), hexStringer{}, nil
|
||||||
|
case "murmur2":
|
||||||
|
return murmur2.New(), numberStringer{}, nil
|
||||||
}
|
}
|
||||||
// TODO: implement murmur2
|
return nil, nil, fmt.Errorf("hash implementation %s not found", hashType)
|
||||||
return nil, errors.New("hash implementation not found")
|
}
|
||||||
|
|
||||||
|
type HashStringer interface {
|
||||||
|
HashToString([]byte) string
|
||||||
|
}
|
||||||
|
|
||||||
|
type hexStringer struct{}
|
||||||
|
|
||||||
|
func (hexStringer) HashToString(data []byte) string {
|
||||||
|
return hex.EncodeToString(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
type numberStringer struct{}
|
||||||
|
|
||||||
|
func (numberStringer) HashToString(data []byte) string {
|
||||||
|
return strconv.FormatUint(uint64(binary.BigEndian.Uint32(data)), 10)
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
@ -133,7 +132,7 @@ func (in *Index) updateFile(path string) error {
|
|||||||
// Hash usage strategy (may change):
|
// Hash usage strategy (may change):
|
||||||
// Just use SHA256, overwrite existing hash regardless of what it is
|
// Just use SHA256, overwrite existing hash regardless of what it is
|
||||||
// May update later to continue using the same hash that was already being used
|
// May update later to continue using the same hash that was already being used
|
||||||
h, err := GetHashImpl("sha256")
|
h, stringer, err := GetHashImpl("sha256")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return err
|
return err
|
||||||
@ -146,7 +145,7 @@ func (in *Index) updateFile(path string) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hashString = hex.EncodeToString(h.Sum(nil))
|
hashString = stringer.HashToString(h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
mod := false
|
mod := false
|
||||||
@ -348,7 +347,7 @@ func (in Index) SaveFile(f IndexFile, dest io.Writer) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
h, err := GetHashImpl(hashFormat)
|
h, stringer, err := GetHashImpl(hashFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -359,7 +358,7 @@ func (in Index) SaveFile(f IndexFile, dest io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedHash := hex.EncodeToString(h.Sum(nil))
|
calculatedHash := stringer.HashToString(h.Sum(nil))
|
||||||
if calculatedHash != f.Hash && !viper.GetBool("no-internal-hashes") {
|
if calculatedHash != f.Hash && !viper.GetBool("no-internal-hashes") {
|
||||||
return errors.New("hash of saved file is invalid")
|
return errors.New("hash of saved file is invalid")
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -90,7 +89,7 @@ func (m Mod) Write() (string, string, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
h, err := GetHashImpl("sha256")
|
h, stringer, err := GetHashImpl("sha256")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return "", "", err
|
return "", "", err
|
||||||
@ -101,7 +100,7 @@ func (m Mod) Write() (string, string, error) {
|
|||||||
// Disable indentation
|
// Disable indentation
|
||||||
enc.Indent = ""
|
enc.Indent = ""
|
||||||
err = enc.Encode(m)
|
err = enc.Encode(m)
|
||||||
hashString := hex.EncodeToString(h.Sum(nil))
|
hashString := stringer.HashToString(h.Sum(nil))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return "sha256", hashString, err
|
return "sha256", hashString, err
|
||||||
@ -135,7 +134,7 @@ func (m Mod) DownloadFile(dest io.Writer) error {
|
|||||||
_ = resp.Body.Close()
|
_ = resp.Body.Close()
|
||||||
return errors.New("invalid status code " + strconv.Itoa(resp.StatusCode))
|
return errors.New("invalid status code " + strconv.Itoa(resp.StatusCode))
|
||||||
}
|
}
|
||||||
h, err := GetHashImpl(m.Download.HashFormat)
|
h, stringer, err := GetHashImpl(m.Download.HashFormat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -146,7 +145,7 @@ func (m Mod) DownloadFile(dest io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
calculatedHash := hex.EncodeToString(h.Sum(nil))
|
calculatedHash := stringer.HashToString(h.Sum(nil))
|
||||||
|
|
||||||
// Check if the hash of the downloaded file matches the expected hash.
|
// Check if the hash of the downloaded file matches the expected hash.
|
||||||
if calculatedHash != m.Download.Hash {
|
if calculatedHash != m.Download.Hash {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package core
|
package core
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -116,7 +115,7 @@ func (pack *Pack) UpdateIndexHash() error {
|
|||||||
// Hash usage strategy (may change):
|
// Hash usage strategy (may change):
|
||||||
// Just use SHA256, overwrite existing hash regardless of what it is
|
// Just use SHA256, overwrite existing hash regardless of what it is
|
||||||
// May update later to continue using the same hash that was already being used
|
// May update later to continue using the same hash that was already being used
|
||||||
h, err := GetHashImpl("sha256")
|
h, stringer, err := GetHashImpl("sha256")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return err
|
return err
|
||||||
@ -125,7 +124,7 @@ func (pack *Pack) UpdateIndexHash() error {
|
|||||||
_ = f.Close()
|
_ = f.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
hashString := hex.EncodeToString(h.Sum(nil))
|
hashString := stringer.HashToString(h.Sum(nil))
|
||||||
|
|
||||||
pack.Index.HashFormat = "sha256"
|
pack.Index.HashFormat = "sha256"
|
||||||
pack.Index.Hash = hashString
|
pack.Index.Hash = hashString
|
||||||
|
54
curseforge/murmur2/hash.go
Normal file
54
curseforge/murmur2/hash.go
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
package murmur2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"github.com/aviddiviner/go-murmur"
|
||||||
|
"hash"
|
||||||
|
)
|
||||||
|
|
||||||
|
func New() hash.Hash32 {
|
||||||
|
return &Murmur2CF{buf: make([]byte, 0)}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Murmur2CF struct {
|
||||||
|
// Can't be done incrementally, since it is seeded with the length of the input!
|
||||||
|
buf []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) Write(p []byte) (n int, err error) {
|
||||||
|
for _, b := range p {
|
||||||
|
if !isWhitespaceCharacter(b) {
|
||||||
|
m.buf = append(m.buf, b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return len(p), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CF modification: strips whitespace characters
|
||||||
|
func isWhitespaceCharacter(b byte) bool {
|
||||||
|
return b == 9 || b == 10 || b == 13 || b == 32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) Sum(b []byte) []byte {
|
||||||
|
if b == nil {
|
||||||
|
b = make([]byte, 4)
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint32(b, murmur.MurmurHash2(m.buf, 1))
|
||||||
|
return b
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) Reset() {
|
||||||
|
m.buf = make([]byte, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) Size() int {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) BlockSize() int {
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *Murmur2CF) Sum32() uint32 {
|
||||||
|
return binary.BigEndian.Uint32(m.Sum(nil))
|
||||||
|
}
|
243
modrinth/export.go
Normal file
243
modrinth/export.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
package modrinth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/packwiz/packwiz/core"
|
||||||
|
"github.com/spf13/cobra"
|
||||||
|
)
|
||||||
|
|
||||||
|
// exportCmd represents the export command
|
||||||
|
var exportCmd = &cobra.Command{
|
||||||
|
Use: "export",
|
||||||
|
Short: "Export the current modpack into a .mrpack for Modrinth",
|
||||||
|
Args: cobra.NoArgs,
|
||||||
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
|
fmt.Println("Loading modpack...")
|
||||||
|
pack, err := core.LoadPack()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
index, err := pack.LoadIndex()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// Do a refresh to ensure files are up to date
|
||||||
|
err = index.Refresh()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = index.Write()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = pack.UpdateIndexHash()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = pack.Write()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: should index just expose indexPath itself, through a function?
|
||||||
|
indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))
|
||||||
|
|
||||||
|
mods := loadMods(index)
|
||||||
|
|
||||||
|
var fileName = pack.GetPackName() + ".mrpack"
|
||||||
|
expFile, err := os.Create(fileName)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to create zip: %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
exp := zip.NewWriter(expFile)
|
||||||
|
|
||||||
|
// Add an overrides folder even if there are no files to go in it
|
||||||
|
_, err = exp.Create("overrides/")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to add overrides folder: %s\n", err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: cache these (ideally with changes to pack format)
|
||||||
|
fmt.Println("Retrieving SHA1 hashes for external mods...")
|
||||||
|
sha1Hashes := make([]string, len(mods))
|
||||||
|
for i, mod := range mods {
|
||||||
|
if mod.Download.HashFormat == "sha1" {
|
||||||
|
sha1Hashes[i] = mod.Download.Hash
|
||||||
|
} else {
|
||||||
|
// Hash format for this mod isn't SHA1 - and the Modrinth pack format requires it; so get it by downloading the file
|
||||||
|
h, stringer, err := core.GetHashImpl("sha1")
|
||||||
|
if err != nil {
|
||||||
|
panic("Failed to get sha1 hash implementation")
|
||||||
|
}
|
||||||
|
err = mod.DownloadFile(h)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error downloading mod file %s: %s\n", mod.Download.URL, err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
sha1Hashes[i] = stringer.HashToString(h.Sum(nil))
|
||||||
|
fmt.Printf("Retrieved SHA1 hash for %s successfully\n", mod.Download.URL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestFile, err := exp.Create("modrinth.index.json")
|
||||||
|
if err != nil {
|
||||||
|
_ = exp.Close()
|
||||||
|
_ = expFile.Close()
|
||||||
|
fmt.Println("Error creating manifest: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestFiles := make([]PackFile, len(mods))
|
||||||
|
for i, mod := range mods {
|
||||||
|
pathForward, err := filepath.Rel(filepath.Dir(indexPath), mod.GetDestFilePath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error resolving mod file: %s\n", err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.ToSlash(pathForward)
|
||||||
|
|
||||||
|
hashes := make(map[string]string)
|
||||||
|
hashes["sha1"] = sha1Hashes[i]
|
||||||
|
|
||||||
|
// Create env options based on configured optional/side
|
||||||
|
var envInstalled string
|
||||||
|
if mod.Option != nil && mod.Option.Optional {
|
||||||
|
envInstalled = "optional"
|
||||||
|
} else {
|
||||||
|
envInstalled = "required"
|
||||||
|
}
|
||||||
|
var clientEnv, serverEnv string
|
||||||
|
if mod.Side == core.UniversalSide {
|
||||||
|
clientEnv = envInstalled
|
||||||
|
serverEnv = envInstalled
|
||||||
|
} else if mod.Side == core.ClientSide {
|
||||||
|
clientEnv = envInstalled
|
||||||
|
serverEnv = "unsupported"
|
||||||
|
} else if mod.Side == core.ServerSide {
|
||||||
|
clientEnv = "unsupported"
|
||||||
|
serverEnv = envInstalled
|
||||||
|
}
|
||||||
|
|
||||||
|
manifestFiles[i] = PackFile{
|
||||||
|
Path: path,
|
||||||
|
Hashes: hashes,
|
||||||
|
Env: &struct {
|
||||||
|
Client string `json:"client"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
}{Client: clientEnv, Server: serverEnv},
|
||||||
|
Downloads: []string{mod.Download.URL},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies := make(map[string]string)
|
||||||
|
dependencies["minecraft"], err = pack.GetMCVersion()
|
||||||
|
if err != nil {
|
||||||
|
_ = exp.Close()
|
||||||
|
_ = expFile.Close()
|
||||||
|
fmt.Println("Error creating manifest: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if fabricVersion, ok := pack.Versions["fabric"]; ok {
|
||||||
|
dependencies["fabric-loader"] = fabricVersion
|
||||||
|
} else if forgeVersion, ok := pack.Versions["forge"]; ok {
|
||||||
|
dependencies["forge"] = forgeVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
manifest := Pack{
|
||||||
|
FormatVersion: 1,
|
||||||
|
Game: "minecraft",
|
||||||
|
VersionID: pack.Version,
|
||||||
|
Name: pack.Name,
|
||||||
|
Files: manifestFiles,
|
||||||
|
Dependencies: dependencies,
|
||||||
|
}
|
||||||
|
|
||||||
|
w := json.NewEncoder(manifestFile)
|
||||||
|
w.SetIndent("", " ") // Documentation uses 4 spaces
|
||||||
|
err = w.Encode(manifest)
|
||||||
|
if err != nil {
|
||||||
|
_ = exp.Close()
|
||||||
|
_ = expFile.Close()
|
||||||
|
fmt.Println("Error writing manifest: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range index.Files {
|
||||||
|
if !v.MetaFile {
|
||||||
|
// Save all non-metadata files into the zip
|
||||||
|
path, err := filepath.Rel(filepath.Dir(indexPath), index.GetFilePath(v))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error resolving file: %s\n", err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
file, err := exp.Create(filepath.ToSlash(filepath.Join("overrides", path)))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error creating file: %s\n", err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err = index.SaveFile(v, file)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error copying file: %s\n", err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = exp.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing export file: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
err = expFile.Close()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Error writing export file: " + err.Error())
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Modpack exported to " + fileName)
|
||||||
|
fmt.Println("Make sure you remove this file before running packwiz refresh, or add it to .packwizignore")
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadMods(index core.Index) []core.Mod {
|
||||||
|
modPaths := index.GetAllMods()
|
||||||
|
mods := make([]core.Mod, len(modPaths))
|
||||||
|
i := 0
|
||||||
|
fmt.Println("Reading mod files...")
|
||||||
|
for _, v := range modPaths {
|
||||||
|
modData, err := core.LoadMod(v)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Error reading mod file %s: %s\n", v, err.Error())
|
||||||
|
// TODO: exit(1)?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
mods[i] = modData
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
return mods[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
modrinthCmd.AddCommand(exportCmd)
|
||||||
|
}
|
22
modrinth/pack.go
Normal file
22
modrinth/pack.go
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
package modrinth
|
||||||
|
|
||||||
|
type Pack struct {
|
||||||
|
FormatVersion int `json:"formatVersion"`
|
||||||
|
Game string `json:"game"`
|
||||||
|
VersionID string `json:"versionId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
// TODO: implement Summary
|
||||||
|
// Summary string `json:"summary"`
|
||||||
|
Files []PackFile `json:"files"`
|
||||||
|
Dependencies map[string]string `json:"dependencies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PackFile struct {
|
||||||
|
Path string `json:"path"`
|
||||||
|
Hashes map[string]string `json:"hashes"`
|
||||||
|
Env *struct {
|
||||||
|
Client string `json:"client"`
|
||||||
|
Server string `json:"server"`
|
||||||
|
} `json:"env"`
|
||||||
|
Downloads []string `json:"downloads"`
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user