mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 21:16:30 +02:00
Implement pack importing/exporting for downloaded Curseforge packs
Abstract out hash implementations Implement file saving/downloading
This commit is contained in:
parent
73f6184b3d
commit
5dfe23e51d
23
core/hash.go
Normal file
23
core/hash.go
Normal 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")
|
||||
}
|
@ -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
|
||||
}
|
||||
|
42
core/mod.go
42
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
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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("<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)
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
81
curseforge/packinterop/disk.go
Normal file
81
curseforge/packinterop/disk.go
Normal 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,
|
||||
}
|
||||
}
|
25
curseforge/packinterop/interfaces.go
Normal file
25
curseforge/packinterop/interfaces.go
Normal 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
|
||||
}
|
104
curseforge/packinterop/manifest.go
Normal file
104
curseforge/packinterop/manifest.go
Normal 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
|
||||
}
|
85
curseforge/packinterop/minecraftinstance.go
Normal file
85
curseforge/packinterop/minecraftinstance.go
Normal 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
|
||||
}
|
119
curseforge/packinterop/translation.go
Normal file
119
curseforge/packinterop/translation.go
Normal 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
|
||||
}
|
57
curseforge/packinterop/zip.go
Normal file
57
curseforge/packinterop/zip.go
Normal 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,
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user