diff --git a/core/hash.go b/core/hash.go
new file mode 100644
index 0000000..ef05119
--- /dev/null
+++ b/core/hash.go
@@ -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")
+}
diff --git a/core/index.go b/core/index.go
index 1cd9b85..2eb558e 100644
--- a/core/index.go
+++ b/core/index.go
@@ -1,8 +1,8 @@
package core
import (
- "crypto/sha256"
"encoding/hex"
+ "errors"
"io"
"os"
"path/filepath"
@@ -130,7 +130,10 @@ func (in *Index) updateFile(path string) error {
// Hash usage strategy (may change):
// 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
- h := sha256.New()
+ h, err := GetHashImpl("sha256")
+ if err != nil {
+ return err
+ }
if _, err := io.Copy(h, f); err != nil {
return err
}
@@ -310,3 +313,37 @@ func (in Index) GetAllMods() []string {
}
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
+}
diff --git a/core/mod.go b/core/mod.go
index 2bfce5e..de08ef2 100644
--- a/core/mod.go
+++ b/core/mod.go
@@ -1,12 +1,13 @@
package core
import (
- "crypto/sha256"
"encoding/hex"
"errors"
"io"
+ "net/http"
"os"
"path/filepath"
+ "strconv"
"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".
+//noinspection GoUnusedConst
const (
ServerSide = "server"
ClientSide = "client"
@@ -88,7 +90,10 @@ func (m Mod) Write() (string, string, error) {
}
defer f.Close()
- h := sha256.New()
+ h, err := GetHashImpl("sha256")
+ if err != nil {
+ return "", "", err
+ }
w := io.MultiWriter(h, f)
enc := toml.NewEncoder(w)
@@ -109,3 +114,36 @@ func (m Mod) GetParsedUpdateData(updaterName string) (interface{}, bool) {
func (m Mod) GetFilePath() string {
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
+}
diff --git a/core/pack.go b/core/pack.go
index ec8dbd0..972fe3b 100644
--- a/core/pack.go
+++ b/core/pack.go
@@ -1,7 +1,6 @@
package core
import (
- "crypto/sha256"
"encoding/hex"
"errors"
"io"
@@ -65,7 +64,10 @@ func (pack *Pack) UpdateIndexHash() error {
// Hash usage strategy (may change):
// 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
- h := sha256.New()
+ h, err := GetHashImpl("sha256")
+ if err != nil {
+ return err
+ }
if _, err := io.Copy(h, f); err != nil {
return err
}
diff --git a/curseforge/export.go b/curseforge/export.go
index 7e951a9..7d5bd09 100644
--- a/curseforge/export.go
+++ b/curseforge/export.go
@@ -1,8 +1,15 @@
package curseforge
import (
+ "archive/zip"
+ "bufio"
"fmt"
+ "github.com/spf13/viper"
"os"
+ "path/filepath"
+ "strconv"
+
+ "github.com/comp500/packwiz/curseforge/packinterop"
"github.com/comp500/packwiz/core"
"github.com/spf13/cobra"
@@ -12,7 +19,7 @@ import (
var exportCmd = &cobra.Command{
Use: "export",
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,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("Loading modpack...")
@@ -27,6 +34,155 @@ var exportCmd = &cobra.Command{
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("
\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("- " + mod.Name + "
\r\n")
+ if err != nil {
+ return err
+ }
+ continue
+ }
+ project := projectRaw.(cfUpdateData)
+ projIDString := strconv.Itoa(project.ProjectID)
+ _, err = w.WriteString("- " + mod.Name + "
\r\n")
+ if err != nil {
+ return err
+ }
+ }
+ _, err = w.WriteString("
\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)
+}
diff --git a/curseforge/import.go b/curseforge/import.go
index d5bea39..5c95401 100644
--- a/curseforge/import.go
+++ b/curseforge/import.go
@@ -4,9 +4,9 @@ import (
"archive/zip"
"bufio"
"bytes"
- "encoding/json"
"errors"
"fmt"
+ "github.com/comp500/packwiz/curseforge/packinterop"
"io"
"io/ioutil"
"os"
@@ -19,43 +19,20 @@ import (
"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
var importCmd = &cobra.Command{
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),
Run: func(cmd *cobra.Command, args []string) {
inputFile := args[0]
- var packImport importPackMetadata
+ var packImport packinterop.ImportPackMetadata
+ // TODO: refactor/extract file checking?
if strings.HasPrefix(inputFile, "http") {
- fmt.Println("it do be a http doe")
- os.Exit(0)
+ // TODO: implement
+ fmt.Println("HTTP not supported (yet)")
+ os.Exit(1)
} else {
// Attempt to read from file
var f *os.File
@@ -140,8 +117,7 @@ var importCmd = &cobra.Command{
// Search the zip for minecraftinstance.json or manifest.json
var metaFile *zip.File
for _, v := range zr.File {
- fileName := filepath.Base(v.Name)
- if fileName == "minecraftinstance.json" || fileName == "manifest.json" {
+ if v.Name == "minecraftinstance.json" || v.Name == "manifest.json" {
metaFile = v
}
}
@@ -151,17 +127,9 @@ var importCmd = &cobra.Command{
os.Exit(1)
}
- packImport = readMetadata(zipPackSource{
- MetaFile: metaFile,
- Reader: zr,
- })
-
+ packImport = packinterop.ReadMetadata(packinterop.GetZipPackSource(metaFile, zr))
} else {
- packImport = readMetadata(diskPackSource{
- MetaSource: buf,
- MetaName: inputFile, // TODO: is this always the correct file?
- BasePath: filepath.Dir(inputFile),
- })
+ packImport = packinterop.ReadMetadata(packinterop.GetDiskPackSource(buf, filepath.ToSlash(filepath.Base(inputFile)), filepath.Dir(inputFile)))
}
}
@@ -255,13 +223,10 @@ var importCmd = &cobra.Command{
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))
if err == nil {
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)
@@ -280,10 +245,7 @@ var importCmd = &cobra.Command{
successes = 0
indexFolder := filepath.Dir(filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File)))
for _, v := range filesList {
- filePath := v.Name()
- if !filepath.IsAbs(filePath) {
- filePath = filepath.Join(indexFolder, v.Name())
- }
+ filePath := filepath.Join(indexFolder, filepath.FromSlash(v.Name()))
filePathAbs, err := filepath.Abs(filePath)
if err == nil {
found := false
@@ -298,12 +260,12 @@ var importCmd = &cobra.Command{
successes++
continue
}
- if filepath.Base(filePathAbs) == "minecraftinstance.json" {
+ if v.Name() == "minecraftinstance.json" {
fmt.Println("Ignored file \"minecraftinstance.json\"")
successes++
continue
}
- if filepath.Base(filePathAbs) == "manifest.json" {
+ if v.Name() == "manifest.json" {
fmt.Println("Ignored file \"manifest.json\"")
successes++
continue
@@ -376,240 +338,3 @@ var importCmd = &cobra.Command{
func init() {
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
-}
diff --git a/curseforge/packinterop/disk.go b/curseforge/packinterop/disk.go
new file mode 100644
index 0000000..f302bd1
--- /dev/null
+++ b/curseforge/packinterop/disk.go
@@ -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,
+ }
+}
diff --git a/curseforge/packinterop/interfaces.go b/curseforge/packinterop/interfaces.go
new file mode 100644
index 0000000..beca2c9
--- /dev/null
+++ b/curseforge/packinterop/interfaces.go
@@ -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
+}
diff --git a/curseforge/packinterop/manifest.go b/curseforge/packinterop/manifest.go
new file mode 100644
index 0000000..06a226b
--- /dev/null
+++ b/curseforge/packinterop/manifest.go
@@ -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
+}
diff --git a/curseforge/packinterop/minecraftinstance.go b/curseforge/packinterop/minecraftinstance.go
new file mode 100644
index 0000000..0e161df
--- /dev/null
+++ b/curseforge/packinterop/minecraftinstance.go
@@ -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
+}
diff --git a/curseforge/packinterop/translation.go b/curseforge/packinterop/translation.go
new file mode 100644
index 0000000..c7f454a
--- /dev/null
+++ b/curseforge/packinterop/translation.go
@@ -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
+}
diff --git a/curseforge/packinterop/zip.go b/curseforge/packinterop/zip.go
new file mode 100644
index 0000000..73c311f
--- /dev/null
+++ b/curseforge/packinterop/zip.go
@@ -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,
+ }
+}