mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 21:16:30 +02:00
615 lines
15 KiB
Go
615 lines
15 KiB
Go
package curseforge
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/comp500/packwiz/core"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// TODO: this file is a mess, I need to refactor it
|
|
|
|
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",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
inputFile := args[0]
|
|
var packImport importPackMetadata
|
|
|
|
if strings.HasPrefix(inputFile, "http") {
|
|
fmt.Println("it do be a http doe")
|
|
os.Exit(0)
|
|
} else {
|
|
// Attempt to read from file
|
|
var f *os.File
|
|
inputFileStat, err := os.Stat(inputFile)
|
|
if err == nil && inputFileStat.IsDir() {
|
|
// Apparently os.Open doesn't fail when file given is a directory, only when it gets read
|
|
err = errors.New("cannot open directory")
|
|
}
|
|
if err == nil {
|
|
f, err = os.Open(inputFile)
|
|
}
|
|
if err != nil {
|
|
found := false
|
|
var errInstance error
|
|
var errManifest error
|
|
var errCurse error
|
|
|
|
// Look for other files/folders
|
|
if _, errInstance = os.Stat(filepath.Join(inputFile, "minecraftinstance.json")); errInstance == nil {
|
|
inputFile = filepath.Join(inputFile, "minecraftinstance.json")
|
|
found = true
|
|
} else if _, errManifest = os.Stat(filepath.Join(inputFile, "manifest.json")); errManifest == nil {
|
|
inputFile = filepath.Join(inputFile, "manifest.json")
|
|
found = true
|
|
} else if runtime.GOOS == "windows" {
|
|
var dir string
|
|
dir, errCurse = getCurseDir()
|
|
if errCurse == nil {
|
|
curseInstanceFile := filepath.Join(dir, "Minecraft", "Instances", inputFile, "minecraftinstance.json")
|
|
if _, errCurse = os.Stat(curseInstanceFile); errCurse == nil {
|
|
inputFile = curseInstanceFile
|
|
found = true
|
|
}
|
|
}
|
|
}
|
|
|
|
if found {
|
|
f, err = os.Open(inputFile)
|
|
if err != nil {
|
|
fmt.Printf("Error opening file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
fmt.Printf("Error opening file: %s\n", err)
|
|
fmt.Printf("Also attempted minecraftinstance.json: %s\n", errInstance)
|
|
fmt.Printf("Also attempted manifest.json: %s\n", errManifest)
|
|
if errCurse != nil {
|
|
fmt.Printf("Also attempted to load a Curse/Twitch modpack named \"%s\": %s\n", inputFile, errCurse)
|
|
}
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
defer f.Close()
|
|
|
|
buf := bufio.NewReader(f)
|
|
header, err := buf.Peek(2)
|
|
if err != nil {
|
|
fmt.Printf("Error reading file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// Check if file is a zip
|
|
if string(header) == "PK" {
|
|
// Read the whole file (as bufio doesn't work for zips)
|
|
zipData, err := ioutil.ReadAll(buf)
|
|
if err != nil {
|
|
fmt.Printf("Error reading file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
// Get zip size
|
|
stat, err := f.Stat()
|
|
if err != nil {
|
|
fmt.Printf("Error reading file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
zr, err := zip.NewReader(bytes.NewReader(zipData), stat.Size())
|
|
if err != nil {
|
|
fmt.Printf("Error parsing zip: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
// 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" {
|
|
metaFile = v
|
|
}
|
|
}
|
|
|
|
if metaFile == nil {
|
|
fmt.Println("Can't find manifest.json or minecraftinstance.json, is this a valid pack?")
|
|
os.Exit(1)
|
|
}
|
|
|
|
packImport = readMetadata(zipPackSource{
|
|
MetaFile: metaFile,
|
|
Reader: zr,
|
|
})
|
|
|
|
} else {
|
|
packImport = readMetadata(diskPackSource{
|
|
MetaSource: buf,
|
|
MetaName: inputFile, // TODO: is this always the correct file?
|
|
BasePath: filepath.Dir(inputFile),
|
|
})
|
|
}
|
|
}
|
|
|
|
pack, err := core.LoadPack()
|
|
if err != nil {
|
|
fmt.Println("Failed to load existing pack, creating a new one...")
|
|
|
|
// Create a new modpack
|
|
indexFilePath := viper.GetString("init.index-file")
|
|
_, err = os.Stat(indexFilePath)
|
|
if os.IsNotExist(err) {
|
|
// Create file
|
|
err = ioutil.WriteFile(indexFilePath, []byte{}, 0644)
|
|
if err != nil {
|
|
fmt.Printf("Error creating index file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
fmt.Println(indexFilePath + " created!")
|
|
} else if err != nil {
|
|
fmt.Printf("Error checking index file: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
pack = core.Pack{
|
|
Name: packImport.Name(),
|
|
Index: struct {
|
|
File string `toml:"file"`
|
|
HashFormat string `toml:"hash-format"`
|
|
Hash string `toml:"hash"`
|
|
}{
|
|
File: indexFilePath,
|
|
},
|
|
Versions: packImport.Versions(),
|
|
}
|
|
}
|
|
index, err := pack.LoadIndex()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
modsList := packImport.Mods()
|
|
modIDs := make([]int, len(modsList))
|
|
for i, v := range modsList {
|
|
modIDs[i] = v.ModID
|
|
}
|
|
|
|
fmt.Println("Querying Curse API for mod info...")
|
|
|
|
modInfos, err := getModInfoMultiple(modIDs)
|
|
if err != nil {
|
|
fmt.Printf("Failed to obtain mod information: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
modInfosMap := make(map[int]modInfo)
|
|
for _, v := range modInfos {
|
|
modInfosMap[v.ID] = v
|
|
}
|
|
|
|
// TODO: multithreading????
|
|
|
|
referencedModPaths := make([]string, 0, len(modsList))
|
|
successes := 0
|
|
for _, v := range modsList {
|
|
modInfoValue, ok := modInfosMap[v.ModID]
|
|
if !ok {
|
|
fmt.Printf("Failed to obtain mod information for ID %d\n", v.ModID)
|
|
continue
|
|
}
|
|
|
|
found := false
|
|
var fileInfo modFileInfo
|
|
for _, fileInfo = range modInfoValue.LatestFiles {
|
|
if fileInfo.ID == v.FileID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
fileInfo, err = getFileInfo(v.ModID, v.FileID)
|
|
if err != nil {
|
|
fmt.Printf("Failed to obtain file information for Mod / File %d / %d: %s\n", v.ModID, v.FileID, err)
|
|
continue
|
|
}
|
|
}
|
|
|
|
err = createModFile(modInfoValue, fileInfo, &index)
|
|
if err != nil {
|
|
fmt.Printf("Failed to save mod \"%s\": %s\n", modInfoValue.Name, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
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)
|
|
successes++
|
|
}
|
|
|
|
fmt.Printf("Successfully imported %d/%d mods!\n", successes, len(modsList))
|
|
|
|
fmt.Println("Reading override files...")
|
|
filesList, err := packImport.GetFiles()
|
|
if err != nil {
|
|
fmt.Printf("Failed to read override files: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
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())
|
|
}
|
|
filePathAbs, err := filepath.Abs(filePath)
|
|
if err == nil {
|
|
found := false
|
|
for _, v := range referencedModPaths {
|
|
if v == filePathAbs {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
fmt.Printf("Ignored file \"%s\" (referenced by metadata)\n", filePath)
|
|
successes++
|
|
continue
|
|
}
|
|
if filepath.Base(filePathAbs) == "minecraftinstance.json" {
|
|
fmt.Println("Ignored file \"minecraftinstance.json\"")
|
|
successes++
|
|
continue
|
|
}
|
|
if filepath.Base(filePathAbs) == "manifest.json" {
|
|
fmt.Println("Ignored file \"manifest.json\"")
|
|
successes++
|
|
continue
|
|
}
|
|
}
|
|
|
|
f, err := os.Create(filePath)
|
|
if err != nil {
|
|
// Attempt to create the containing directory
|
|
err2 := os.MkdirAll(filepath.Dir(filePath), os.ModePerm)
|
|
if err2 == nil {
|
|
f, err = os.Create(filePath)
|
|
}
|
|
if err != nil {
|
|
fmt.Printf("Failed to write file \"%s\": %s\n", filePath, err)
|
|
if err2 != nil {
|
|
fmt.Printf("Failed to create directories: %s\n", err)
|
|
}
|
|
continue
|
|
}
|
|
}
|
|
src, err := v.Open()
|
|
if err != nil {
|
|
fmt.Printf("Failed to read file \"%s\": %s\n", filePath, err)
|
|
f.Close()
|
|
continue
|
|
}
|
|
_, err = io.Copy(f, src)
|
|
if err != nil {
|
|
fmt.Printf("Failed to copy file \"%s\": %s\n", filePath, err)
|
|
f.Close()
|
|
src.Close()
|
|
continue
|
|
}
|
|
|
|
fmt.Printf("Copied file \"%s\" successfully!\n", filePath)
|
|
f.Close()
|
|
src.Close()
|
|
successes++
|
|
}
|
|
if len(filesList) > 0 {
|
|
fmt.Printf("Successfully copied %d/%d files!\n", successes, len(filesList))
|
|
err = index.Refresh()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
fmt.Println("No files copied!")
|
|
}
|
|
|
|
err = index.Write()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
err = pack.UpdateIndexHash()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
err = pack.Write()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
},
|
|
}
|
|
|
|
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
|
|
}
|