Implement pack importing/exporting for downloaded Curseforge packs

Abstract out hash implementations
Implement file saving/downloading
This commit is contained in:
comp500 2019-11-12 22:11:40 +00:00
parent 73f6184b3d
commit 5dfe23e51d
12 changed files with 749 additions and 297 deletions

23
core/hash.go Normal file
View File

@ -0,0 +1,23 @@
package core
import (
"crypto/md5"
"crypto/sha256"
"crypto/sha512"
"errors"
"hash"
)
// GetHashImpl gets an implementation of hash.Hash for the given hash type string
func GetHashImpl(hashType string) (hash.Hash, error) {
switch hashType {
case "sha256":
return sha256.New(), nil
case "sha512":
return sha512.New(), nil
case "md5":
return md5.New(), nil
}
// TODO: implement murmur2
return nil, errors.New("hash implementation not found")
}

View File

@ -1,8 +1,8 @@
package core package core
import ( import (
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"io" "io"
"os" "os"
"path/filepath" "path/filepath"
@ -130,7 +130,10 @@ 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 := sha256.New() h, err := GetHashImpl("sha256")
if err != nil {
return err
}
if _, err := io.Copy(h, f); err != nil { if _, err := io.Copy(h, f); err != nil {
return err return err
} }
@ -310,3 +313,37 @@ func (in Index) GetAllMods() []string {
} }
return list return list
} }
// GetFilePath attempts to get the path of the destination index file as it is stored on disk
func (in Index) GetFilePath(f IndexFile) string {
return filepath.Join(filepath.Dir(in.indexFile), filepath.FromSlash(f.File))
}
// SaveFile attempts to read the file from disk
func (in Index) SaveFile(f IndexFile, dest io.Writer) error {
hashFormat := f.HashFormat
if hashFormat == "" {
hashFormat = in.HashFormat
}
src, err := os.Open(in.GetFilePath(f))
if err != nil {
return err
}
h, err := GetHashImpl(hashFormat)
if err != nil {
return err
}
w := io.MultiWriter(h, dest)
_, err = io.Copy(w, src)
if err != nil {
return err
}
calculatedHash := hex.EncodeToString(h.Sum(nil))
if calculatedHash != f.Hash {
return errors.New("hash of saved file is invalid")
}
return nil
}

View File

@ -1,12 +1,13 @@
package core package core
import ( import (
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"io" "io"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"strconv"
"github.com/BurntSushi/toml" "github.com/BurntSushi/toml"
) )
@ -37,6 +38,7 @@ type ModDownload struct {
} }
// The three possible values of Side (the side that the mod is on) are "server", "client", and "both". // The three possible values of Side (the side that the mod is on) are "server", "client", and "both".
//noinspection GoUnusedConst
const ( const (
ServerSide = "server" ServerSide = "server"
ClientSide = "client" ClientSide = "client"
@ -88,7 +90,10 @@ func (m Mod) Write() (string, string, error) {
} }
defer f.Close() defer f.Close()
h := sha256.New() h, err := GetHashImpl("sha256")
if err != nil {
return "", "", err
}
w := io.MultiWriter(h, f) w := io.MultiWriter(h, f)
enc := toml.NewEncoder(w) enc := toml.NewEncoder(w)
@ -109,3 +114,36 @@ func (m Mod) GetParsedUpdateData(updaterName string) (interface{}, bool) {
func (m Mod) GetFilePath() string { func (m Mod) GetFilePath() string {
return m.metaFile return m.metaFile
} }
// GetDestFilePath returns the path of the destination file of the mod
func (m Mod) GetDestFilePath() string {
return filepath.Join(filepath.Dir(m.metaFile), filepath.FromSlash(m.FileName))
}
// DownloadFile attempts to resolve and download the file
func (m Mod) DownloadFile(dest io.Writer) error {
resp, err := http.Get(m.Download.URL)
if err != nil {
return err
}
if resp.StatusCode != 200 {
_ = resp.Body.Close()
return errors.New("invalid status code " + strconv.Itoa(resp.StatusCode))
}
h, err := GetHashImpl(m.Download.HashFormat)
if err != nil {
return err
}
w := io.MultiWriter(h, dest)
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
calculatedHash := hex.EncodeToString(h.Sum(nil))
if calculatedHash != m.Download.Hash {
return errors.New("hash of saved file is invalid")
}
return nil
}

View File

@ -1,7 +1,6 @@
package core package core
import ( import (
"crypto/sha256"
"encoding/hex" "encoding/hex"
"errors" "errors"
"io" "io"
@ -65,7 +64,10 @@ 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 := sha256.New() h, err := GetHashImpl("sha256")
if err != nil {
return err
}
if _, err := io.Copy(h, f); err != nil { if _, err := io.Copy(h, f); err != nil {
return err return err
} }

View File

@ -1,8 +1,15 @@
package curseforge package curseforge
import ( import (
"archive/zip"
"bufio"
"fmt" "fmt"
"github.com/spf13/viper"
"os" "os"
"path/filepath"
"strconv"
"github.com/comp500/packwiz/curseforge/packinterop"
"github.com/comp500/packwiz/core" "github.com/comp500/packwiz/core"
"github.com/spf13/cobra" "github.com/spf13/cobra"
@ -12,7 +19,7 @@ import (
var exportCmd = &cobra.Command{ var exportCmd = &cobra.Command{
Use: "export", Use: "export",
Short: "Export the current modpack into a .zip for curseforge", Short: "Export the current modpack into a .zip for curseforge",
// TODO: argument for file name // TODO: arguments for file name, author? projectID?
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Loading modpack...") fmt.Println("Loading modpack...")
@ -27,6 +34,155 @@ var exportCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
_ = index // TODO: should index just expose indexPath itself, through a function?
indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))
// TODO: filter mods for optional/server/etc
mods := loadMods(index)
expFile, err := os.Create("export.zip")
if err != nil {
fmt.Println(err)
os.Exit(1)
}
defer expFile.Close()
exp := zip.NewWriter(expFile)
defer exp.Close()
cfFileRefs := make([]packinterop.AddonFileReference, 0, len(mods))
for _, mod := range mods {
projectRaw, ok := mod.GetParsedUpdateData("curseforge")
// If the mod has curseforge metadata, add it to cfFileRefs
// TODO: how to handle files with CF metadata, but with different download path?
if ok {
p := projectRaw.(cfUpdateData)
cfFileRefs = append(cfFileRefs, packinterop.AddonFileReference{ProjectID: p.ProjectID, FileID: p.FileID})
} else {
// If the mod doesn't have the metadata, save it into the zip
path, 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
}
modFile, err := exp.Create(filepath.ToSlash(filepath.Join("overrides", path)))
if err != nil {
fmt.Printf("Error creating mod file: %s\n", err.Error())
// TODO: exit(1)?
continue
}
err = mod.DownloadFile(modFile)
if err != nil {
fmt.Printf("Error downloading mod file: %s\n", err.Error())
// TODO: exit(1)?
continue
}
}
}
manifestFile, err := exp.Create("manifest.json")
if err != nil {
fmt.Println("Error creating manifest: " + err.Error())
os.Exit(1)
}
err = packinterop.WriteManifestFromPack(pack, cfFileRefs, manifestFile)
if err != nil {
fmt.Println("Error creating manifest: " + err.Error())
os.Exit(1)
}
err = createModlist(exp, mods)
if err != nil {
fmt.Println("Error creating mod list: " + err.Error())
os.Exit(1)
}
i := 0
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
}
i++
}
}
fmt.Println("Modpack exported to export.zip!")
}, },
} }
func createModlist(zw *zip.Writer, mods []core.Mod) error {
modlistFile, err := zw.Create("modlist.html")
if err != nil {
return err
}
w := bufio.NewWriter(modlistFile)
_, err = w.WriteString("<ul>\r\n")
if err != nil {
return err
}
for _, mod := range mods {
projectRaw, ok := mod.GetParsedUpdateData("curseforge")
if !ok {
// TODO: read homepage URL or something similar?
_, err = w.WriteString("<li>" + mod.Name + "</li>\r\n")
if err != nil {
return err
}
continue
}
project := projectRaw.(cfUpdateData)
projIDString := strconv.Itoa(project.ProjectID)
_, err = w.WriteString("<li><a href=\"https://minecraft.curseforge.com/mc-mods/" + projIDString + "\">" + mod.Name + "</a></li>\r\n")
if err != nil {
return err
}
}
_, err = w.WriteString("</ul>\r\n")
if err != nil {
return err
}
return w.Flush()
}
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\n", err.Error())
// TODO: exit(1)?
continue
}
mods[i] = modData
i++
}
return mods[:i]
}
func init() {
curseforgeCmd.AddCommand(exportCmd)
}

View File

@ -4,9 +4,9 @@ import (
"archive/zip" "archive/zip"
"bufio" "bufio"
"bytes" "bytes"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"github.com/comp500/packwiz/curseforge/packinterop"
"io" "io"
"io/ioutil" "io/ioutil"
"os" "os"
@ -19,43 +19,20 @@ import (
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// TODO: this file is a mess, I need to refactor it
// TODO: test modpack importing before proceeding with further implementation
type importPackFile interface {
Name() string
Open() (io.ReadCloser, error)
}
type importPackMetadata interface {
Name() string
Versions() map[string]string
Mods() []struct {
ModID int
FileID int
}
GetFiles() ([]importPackFile, error)
}
type importPackSource interface {
GetFile(path string) (importPackFile, error)
//TODO: was GetFileList(base string), is it needed?
GetFileList() ([]importPackFile, error)
GetPackFile() importPackFile
}
// importCmd represents the import command // importCmd represents the import command
var importCmd = &cobra.Command{ var importCmd = &cobra.Command{
Use: "import [modpack]", Use: "import [modpack]",
Short: "Import an installed curseforge modpack, from a download URL or a downloaded pack zip, or an installed metadata json file", Short: "Import a curseforge modpack, from a download URL or a downloaded pack zip, or an installed metadata json file",
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
inputFile := args[0] inputFile := args[0]
var packImport importPackMetadata var packImport packinterop.ImportPackMetadata
// TODO: refactor/extract file checking?
if strings.HasPrefix(inputFile, "http") { if strings.HasPrefix(inputFile, "http") {
fmt.Println("it do be a http doe") // TODO: implement
os.Exit(0) fmt.Println("HTTP not supported (yet)")
os.Exit(1)
} else { } else {
// Attempt to read from file // Attempt to read from file
var f *os.File var f *os.File
@ -140,8 +117,7 @@ var importCmd = &cobra.Command{
// Search the zip for minecraftinstance.json or manifest.json // Search the zip for minecraftinstance.json or manifest.json
var metaFile *zip.File var metaFile *zip.File
for _, v := range zr.File { for _, v := range zr.File {
fileName := filepath.Base(v.Name) if v.Name == "minecraftinstance.json" || v.Name == "manifest.json" {
if fileName == "minecraftinstance.json" || fileName == "manifest.json" {
metaFile = v metaFile = v
} }
} }
@ -151,17 +127,9 @@ var importCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
packImport = readMetadata(zipPackSource{ packImport = packinterop.ReadMetadata(packinterop.GetZipPackSource(metaFile, zr))
MetaFile: metaFile,
Reader: zr,
})
} else { } else {
packImport = readMetadata(diskPackSource{ packImport = packinterop.ReadMetadata(packinterop.GetDiskPackSource(buf, filepath.ToSlash(filepath.Base(inputFile)), filepath.Dir(inputFile)))
MetaSource: buf,
MetaName: inputFile, // TODO: is this always the correct file?
BasePath: filepath.Dir(inputFile),
})
} }
} }
@ -255,13 +223,10 @@ var importCmd = &cobra.Command{
os.Exit(1) os.Exit(1)
} }
// TODO: just use mods-folder directly? does texture pack importing affect this?
ref, err := filepath.Abs(filepath.Join(filepath.Dir(core.ResolveMod(modInfoValue.Slug)), fileInfo.FileName)) ref, err := filepath.Abs(filepath.Join(filepath.Dir(core.ResolveMod(modInfoValue.Slug)), fileInfo.FileName))
if err == nil { if err == nil {
referencedModPaths = append(referencedModPaths, ref) referencedModPaths = append(referencedModPaths, ref)
if len(ref) == 0 {
fmt.Println(core.ResolveMod(modInfoValue.Slug))
fmt.Println(filepath.Dir(core.ResolveMod(modInfoValue.Slug)))
}
} }
fmt.Printf("Imported mod \"%s\" successfully!\n", modInfoValue.Name) fmt.Printf("Imported mod \"%s\" successfully!\n", modInfoValue.Name)
@ -280,10 +245,7 @@ var importCmd = &cobra.Command{
successes = 0 successes = 0
indexFolder := filepath.Dir(filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))) indexFolder := filepath.Dir(filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File)))
for _, v := range filesList { for _, v := range filesList {
filePath := v.Name() filePath := filepath.Join(indexFolder, filepath.FromSlash(v.Name()))
if !filepath.IsAbs(filePath) {
filePath = filepath.Join(indexFolder, v.Name())
}
filePathAbs, err := filepath.Abs(filePath) filePathAbs, err := filepath.Abs(filePath)
if err == nil { if err == nil {
found := false found := false
@ -298,12 +260,12 @@ var importCmd = &cobra.Command{
successes++ successes++
continue continue
} }
if filepath.Base(filePathAbs) == "minecraftinstance.json" { if v.Name() == "minecraftinstance.json" {
fmt.Println("Ignored file \"minecraftinstance.json\"") fmt.Println("Ignored file \"minecraftinstance.json\"")
successes++ successes++
continue continue
} }
if filepath.Base(filePathAbs) == "manifest.json" { if v.Name() == "manifest.json" {
fmt.Println("Ignored file \"manifest.json\"") fmt.Println("Ignored file \"manifest.json\"")
successes++ successes++
continue continue
@ -376,240 +338,3 @@ var importCmd = &cobra.Command{
func init() { func init() {
curseforgeCmd.AddCommand(importCmd) curseforgeCmd.AddCommand(importCmd)
} }
type diskFile struct {
NameInternal string
Path string
}
func (f diskFile) Name() string {
return f.NameInternal
}
func (f diskFile) Open() (io.ReadCloser, error) {
return os.Open(f.Path)
}
type zipReaderFile struct {
NameInternal string
*zip.File
}
func (f zipReaderFile) Name() string {
return f.NameInternal
}
type readerFile struct {
NameInternal string
Reader *io.ReadCloser
}
func (f readerFile) Name() string {
return f.NameInternal
}
func (f readerFile) Open() (io.ReadCloser, error) {
return *f.Reader, nil
}
func diskFilesFromPath(base string) ([]importPackFile, error) {
list := make([]importPackFile, 0)
err := filepath.Walk(base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
name, err := filepath.Rel(base, path)
if err != nil {
return err
}
list = append(list, diskFile{name, path})
return nil
})
if err != nil {
return nil, err
}
return list, nil
}
type diskPackSource struct {
MetaSource *bufio.Reader
MetaName string
BasePath string
}
func (s diskPackSource) GetFile(path string) (importPackFile, error) {
return diskFile{s.BasePath, path}, nil
}
func (s diskPackSource) GetFileList() ([]importPackFile, error) {
return diskFilesFromPath(s.BasePath)
}
func (s diskPackSource) GetPackFile() importPackFile {
rc := ioutil.NopCloser(s.MetaSource)
return readerFile{s.MetaName, &rc}
}
type zipPackSource struct {
MetaFile *zip.File
Reader *zip.Reader
cachedFileList []importPackFile
}
func (s zipPackSource) GetFile(path string) (importPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]importPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
for _, v := range s.cachedFileList {
if v.Name() == path {
return v, nil
}
}
return zipReaderFile{}, errors.New("file not found in zip")
}
func (s zipPackSource) GetFileList() ([]importPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]importPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
return s.cachedFileList, nil
}
func (s zipPackSource) GetPackFile() importPackFile {
return zipReaderFile{s.MetaFile.Name, s.MetaFile}
}
type twitchInstalledPackMeta struct {
NameInternal string `json:"name"`
Path string `json:"installPath"`
// TODO: javaArgsOverride?
// TODO: allocatedMemory?
MCVersion string `json:"gameVersion"`
Modloader struct {
Name string `json:"name"`
MavenVersionString string `json:"mavenVersionString"`
} `json:"baseModLoader"`
ModpackOverrides []string `json:"modpackOverrides"`
ModsInternal []struct {
ID int `json:"addonID"`
File struct {
// I've given up on using this cached data, just going to re-request it
ID int `json:"id"`
} `json:"installedFile"`
} `json:"installedAddons"`
// Used to determine if modpackOverrides should be used or not
IsUnlocked bool `json:"isUnlocked"`
srcFile string
}
func (m twitchInstalledPackMeta) Name() string {
return m.NameInternal
}
func (m twitchInstalledPackMeta) Versions() map[string]string {
vers := make(map[string]string)
vers["minecraft"] = m.MCVersion
if strings.HasPrefix(m.Modloader.Name, "forge") {
if len(m.Modloader.MavenVersionString) > 0 {
vers["forge"] = strings.TrimPrefix(m.Modloader.MavenVersionString, "net.minecraftforge:forge:")
} else {
vers["forge"] = m.MCVersion + "-" + strings.TrimPrefix(m.Modloader.Name, "forge-")
}
}
return vers
}
func (m twitchInstalledPackMeta) Mods() []struct {
ModID int
FileID int
} {
list := make([]struct {
ModID int
FileID int
}, len(m.ModsInternal))
for i, v := range m.ModsInternal {
list[i] = struct {
ModID int
FileID int
}{
ModID: v.ID,
FileID: v.File.ID,
}
}
return list
}
func (m twitchInstalledPackMeta) GetFiles() ([]importPackFile, error) {
dir := filepath.Dir(m.srcFile)
if _, err := os.Stat(m.Path); err == nil {
dir = m.Path
}
if m.IsUnlocked {
return diskFilesFromPath(dir)
}
list := make([]importPackFile, len(m.ModpackOverrides))
for i, v := range m.ModpackOverrides {
list[i] = diskFile{
Path: filepath.Join(dir, v),
NameInternal: v,
}
}
return list, nil
}
func readMetadata(s importPackSource) importPackMetadata {
var packImport importPackMetadata
metaFile := s.GetPackFile()
rdr, err := metaFile.Open()
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
os.Exit(1)
}
// Read the whole file (as we are going to parse it multiple times)
fileData, err := ioutil.ReadAll(rdr)
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
os.Exit(1)
}
// Determine what format the file is
var jsonFile map[string]interface{}
err = json.Unmarshal(fileData, &jsonFile)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
isManifest := false
if v, ok := jsonFile["manifestType"]; ok {
isManifest = v.(string) == "minecraftModpack"
}
if isManifest {
fmt.Println("it do be a manifest doe")
os.Exit(0)
// TODO: implement manifest parsing
} else {
// Replace FileNameOnDisk with fileNameOnDisk
fileData = bytes.ReplaceAll(fileData, []byte("FileNameOnDisk"), []byte("fileNameOnDisk"))
packMeta := twitchInstalledPackMeta{}
err = json.Unmarshal(fileData, &packMeta)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
packMeta.srcFile = metaFile.Name()
packImport = packMeta
}
return packImport
}

View File

@ -0,0 +1,81 @@
package packinterop
import (
"bufio"
"io"
"io/ioutil"
"os"
"path/filepath"
)
type diskFile struct {
NameInternal string
Path string
}
func (f diskFile) Name() string {
return f.NameInternal
}
func (f diskFile) Open() (io.ReadCloser, error) {
return os.Open(f.Path)
}
type readerFile struct {
NameInternal string
Reader *io.ReadCloser
}
func (f readerFile) Name() string {
return f.NameInternal
}
func (f readerFile) Open() (io.ReadCloser, error) {
return *f.Reader, nil
}
type diskPackSource struct {
MetaSource *bufio.Reader
MetaName string
BasePath string
}
func (s diskPackSource) GetFile(path string) (ImportPackFile, error) {
return diskFile{path, filepath.Join(s.BasePath, filepath.FromSlash(path))}, nil
}
func (s diskPackSource) GetFileList() ([]ImportPackFile, error) {
list := make([]ImportPackFile, 0)
err := filepath.Walk(s.BasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// Get the name of the file, relative to the pack folder
name, err := filepath.Rel(s.BasePath, path)
if err != nil {
return err
}
list = append(list, diskFile{filepath.ToSlash(name), path})
return nil
})
if err != nil {
return nil, err
}
return list, nil
}
func (s diskPackSource) GetPackFile() ImportPackFile {
rc := ioutil.NopCloser(s.MetaSource)
return readerFile{s.MetaName, &rc}
}
func GetDiskPackSource(metaSource *bufio.Reader, metaName string, basePath string) ImportPackSource {
return diskPackSource{
MetaSource: metaSource,
MetaName: metaName,
BasePath: basePath,
}
}

View File

@ -0,0 +1,25 @@
package packinterop
import "io"
type ImportPackFile interface {
Name() string
Open() (io.ReadCloser, error)
}
type ImportPackMetadata interface {
Name() string
Versions() map[string]string
// TODO: use AddonFileReference?
Mods() []struct {
ModID int
FileID int
}
GetFiles() ([]ImportPackFile, error)
}
type ImportPackSource interface {
GetFile(path string) (ImportPackFile, error)
GetFileList() ([]ImportPackFile, error)
GetPackFile() ImportPackFile
}

View File

@ -0,0 +1,104 @@
package packinterop
import "strings"
type cursePackMeta struct {
Minecraft struct {
Version string `json:"version"`
ModLoaders []modLoaderDef `json:"modLoaders"`
} `json:"minecraft"`
ManifestType string `json:"manifestType"`
ManifestVersion int `json:"manifestVersion"`
NameInternal string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
ProjectID int `json:"projectID"`
Files []struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
} `json:"files"`
Overrides string `json:"overrides"`
importSrc ImportPackSource
}
type modLoaderDef struct {
ID string `json:"id"`
Primary bool `json:"primary"`
}
func (c cursePackMeta) Name() string {
return c.NameInternal
}
func (c cursePackMeta) Versions() map[string]string {
vers := make(map[string]string)
vers["minecraft"] = c.Minecraft.Version
for _, v := range c.Minecraft.ModLoaders {
// Seperate dash-separated modloader/version pairs
parts := strings.SplitN(v.ID, "-", 2)
if len(parts) == 2 {
vers[parts[0]] = parts[1]
}
}
if val, ok := vers["forge"]; ok {
// Remove the minecraft version prefix, if it exists
vers["forge"] = strings.TrimPrefix(val, c.Minecraft.Version+"-")
}
return vers
}
func (c cursePackMeta) Mods() []struct {
ModID int
FileID int
} {
list := make([]struct {
ModID int
FileID int
}, len(c.Files))
for i, v := range c.Files {
list[i] = struct {
ModID int
FileID int
}{
ModID: v.ProjectID,
FileID: v.FileID,
}
}
return list
}
type cursePackOverrideWrapper struct {
name string
ImportPackFile
}
func (w cursePackOverrideWrapper) Name() string {
return w.name
}
func (c cursePackMeta) GetFiles() ([]ImportPackFile, error) {
// Only import files from overrides directory
if len(c.Overrides) > 0 {
fullList, err := c.importSrc.GetFileList()
if err != nil {
return nil, err
}
overridesList := make([]ImportPackFile, 0, len(fullList))
overridesPath := c.Overrides
if !strings.HasSuffix(overridesPath, "/") {
overridesPath = c.Overrides + "/"
}
// Wrap files, removing overrides/ from the start
for _, v := range fullList {
if strings.HasPrefix(v.Name(), overridesPath) {
overridesList = append(overridesList, cursePackOverrideWrapper{
name: strings.TrimPrefix(v.Name(), overridesPath),
ImportPackFile: v,
})
}
}
return overridesList, nil
}
return []ImportPackFile{}, nil
}

View File

@ -0,0 +1,85 @@
package packinterop
import (
"path/filepath"
"strings"
)
type twitchInstalledPackMeta struct {
NameInternal string `json:"name"`
Path string `json:"installPath"`
// TODO: javaArgsOverride?
// TODO: allocatedMemory?
MCVersion string `json:"gameVersion"`
Modloader struct {
Name string `json:"name"`
MavenVersionString string `json:"mavenVersionString"`
} `json:"baseModLoader"`
ModpackOverrides []string `json:"modpackOverrides"`
ModsInternal []struct {
ID int `json:"addonID"`
File struct {
// I've given up on using this cached data, just going to re-request it
ID int `json:"id"`
} `json:"installedFile"`
} `json:"installedAddons"`
// Used to determine if modpackOverrides should be used or not
IsUnlocked bool `json:"isUnlocked"`
importSrc ImportPackSource
}
func (m twitchInstalledPackMeta) Name() string {
return m.NameInternal
}
func (m twitchInstalledPackMeta) Versions() map[string]string {
vers := make(map[string]string)
vers["minecraft"] = m.MCVersion
if strings.HasPrefix(m.Modloader.Name, "forge") {
if len(m.Modloader.MavenVersionString) > 0 {
vers["forge"] = strings.TrimPrefix(m.Modloader.MavenVersionString, "net.minecraftforge:forge:")
} else {
vers["forge"] = strings.TrimPrefix(m.Modloader.Name, "forge-")
}
// Remove the minecraft version prefix, if it exists
vers["forge"] = strings.TrimPrefix(vers["forge"], m.MCVersion+"-")
}
return vers
}
func (m twitchInstalledPackMeta) Mods() []struct {
ModID int
FileID int
} {
list := make([]struct {
ModID int
FileID int
}, len(m.ModsInternal))
for i, v := range m.ModsInternal {
list[i] = struct {
ModID int
FileID int
}{
ModID: v.ID,
FileID: v.File.ID,
}
}
return list
}
func (m twitchInstalledPackMeta) GetFiles() ([]ImportPackFile, error) {
// If the modpack is unlocked, import all the files
// Otherwise import just the modpack overrides
if m.IsUnlocked {
return m.importSrc.GetFileList()
}
list := make([]ImportPackFile, len(m.ModpackOverrides))
var err error
for i, v := range m.ModpackOverrides {
list[i], err = m.importSrc.GetFile(filepath.ToSlash(v))
if err != nil {
return nil, err
}
}
return list, nil
}

View File

@ -0,0 +1,119 @@
package packinterop
import (
"bytes"
"encoding/json"
"fmt"
"github.com/comp500/packwiz/core"
"io"
"io/ioutil"
"os"
)
func ReadMetadata(s ImportPackSource) ImportPackMetadata {
var packImport ImportPackMetadata
metaFile := s.GetPackFile()
rdr, err := metaFile.Open()
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
os.Exit(1)
}
// Read the whole file (as we are going to parse it multiple times)
fileData, err := ioutil.ReadAll(rdr)
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
os.Exit(1)
}
// Determine what format the file is
var jsonFile map[string]interface{}
err = json.Unmarshal(fileData, &jsonFile)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
isManifest := false
if v, ok := jsonFile["manifestType"]; ok {
isManifest = v.(string) == "minecraftModpack"
}
if isManifest {
packMeta := cursePackMeta{importSrc: s}
err = json.Unmarshal(fileData, &packMeta)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
packImport = packMeta
} else {
// Replace FileNameOnDisk with fileNameOnDisk
fileData = bytes.ReplaceAll(fileData, []byte("FileNameOnDisk"), []byte("fileNameOnDisk"))
packMeta := twitchInstalledPackMeta{importSrc: s}
err = json.Unmarshal(fileData, &packMeta)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
packImport = packMeta
}
return packImport
}
// AddonFileReference is a pair of Project ID and File ID to reference a single file on CurseForge
type AddonFileReference struct {
ProjectID int
FileID int
}
func WriteManifestFromPack(pack core.Pack, fileRefs []AddonFileReference, out io.Writer) error {
// TODO: should Required be false sometimes?
files := make([]struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
}, len(fileRefs))
for i, fr := range fileRefs {
files[i] = struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
}{ProjectID: fr.ProjectID, FileID: fr.FileID, Required: true}
}
modLoaders := make([]modLoaderDef, 0, 1)
forgeVersion, ok := pack.Versions["forge"]
if ok {
modLoaders = append(modLoaders, modLoaderDef{
ID: "forge-" + forgeVersion,
Primary: true,
})
}
manifest := cursePackMeta{
Minecraft: struct {
Version string `json:"version"`
ModLoaders []modLoaderDef `json:"modLoaders"`
}{
Version: pack.Versions["minecraft"],
ModLoaders: modLoaders,
},
ManifestType: "minecraftModpack",
ManifestVersion: 1,
NameInternal: pack.Name,
Version: "", // TODO: store or take this?
Author: "", // TODO: store or take this?
ProjectID: 0, // TODO: store or take this?
Files: files,
Overrides: "overrides",
}
w := json.NewEncoder(out)
w.SetIndent("", " ") // Match CF export
err := w.Encode(manifest)
if err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,57 @@
package packinterop
import (
"archive/zip"
"errors"
)
type zipReaderFile struct {
NameInternal string
*zip.File
}
func (f zipReaderFile) Name() string {
return f.NameInternal
}
type zipPackSource struct {
MetaFile *zip.File
Reader *zip.Reader
cachedFileList []ImportPackFile
}
func (s zipPackSource) GetFile(path string) (ImportPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]ImportPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
for _, v := range s.cachedFileList {
if v.Name() == path {
return v, nil
}
}
return zipReaderFile{}, errors.New("file not found in zip")
}
func (s zipPackSource) GetFileList() ([]ImportPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]ImportPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
return s.cachedFileList, nil
}
func (s zipPackSource) GetPackFile() ImportPackFile {
return zipReaderFile{s.MetaFile.Name, s.MetaFile}
}
func GetZipPackSource(metaFile *zip.File, reader *zip.Reader) ImportPackSource {
return zipPackSource{
MetaFile: metaFile,
Reader: reader,
}
}