packwiz/curseforge/import.go
2019-09-19 20:35:26 +01:00

426 lines
10 KiB
Go

package curseforge
import (
"bufio"
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/comp500/packwiz/core"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
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)
}
// 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) {
var packImport importPackMetadata
if strings.HasPrefix(args[0], "http") {
fmt.Println("it do be a http doe")
os.Exit(0)
} else {
// Attempt to read from file
f, err := os.Open(args[0])
if err != nil {
fmt.Printf("Error opening file: %s\n", err)
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" {
fmt.Println("it do be a zip doe")
os.Exit(0)
} else {
// Read the whole file (as we are going to parse it multiple times)
fileData, err := ioutil.ReadAll(buf)
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)
} 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 = args[0]
packImport = packMeta
}
}
}
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)
}
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 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
}