mirror of
				https://github.com/packwiz/packwiz.git
				synced 2025-10-31 02:34:31 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			426 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			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
 | |
| }
 |