mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 21:16:30 +02:00
The mods-folder option is now replaced with two new options: meta-folder and meta-folder-base This allows non-mod files to use the correct directory based on their category; with correct import of resource packs/etc from CurseForge packs, and the ability to override this behaviour. To improve the reliability of packwiz metadata file marking (in the index), new files now use .pw.toml as the extension - any extension can be used, but .pw.toml will now be automatically be marked as a metafile regardless of folder, so you can easily move metadata files around. Existing metadata files will still work (as metafile = true is set in the index); though in the future .pw.toml may be required.
379 lines
10 KiB
Go
379 lines
10 KiB
Go
package curseforge
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/packwiz/packwiz/curseforge/packinterop"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
|
|
"github.com/packwiz/packwiz/core"
|
|
"github.com/spf13/cobra"
|
|
"github.com/spf13/viper"
|
|
)
|
|
|
|
// importCmd represents the import command
|
|
var importCmd = &cobra.Command{
|
|
Use: "import [modpack]",
|
|
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 packinterop.ImportPackMetadata
|
|
|
|
// TODO: refactor/extract file checking?
|
|
if strings.HasPrefix(inputFile, "http") {
|
|
// TODO: implement
|
|
fmt.Println("HTTP not supported (yet)")
|
|
os.Exit(1)
|
|
} 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 {
|
|
if v.Name == "minecraftinstance.json" || v.Name == "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 = packinterop.ReadMetadata(packinterop.GetZipPackSource(metaFile, zr))
|
|
} else {
|
|
packImport = packinterop.ReadMetadata(packinterop.GetDiskPackSource(buf, filepath.ToSlash(filepath.Base(inputFile)), 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(),
|
|
Author: packImport.PackAuthor(),
|
|
Version: packImport.PackVersion(),
|
|
PackFormat: core.CurrentPackFormat,
|
|
Index: struct {
|
|
File string `toml:"file"`
|
|
HashFormat string `toml:"hash-format"`
|
|
Hash string `toml:"hash,omitempty"`
|
|
}{
|
|
File: indexFilePath,
|
|
},
|
|
Versions: packImport.Versions(),
|
|
}
|
|
} else {
|
|
for component, version := range packImport.Versions() {
|
|
packVersion, ok := pack.Versions[component]
|
|
if !ok {
|
|
fmt.Println("Set " + core.ComponentToFriendlyName(component) + " version to " + version)
|
|
} else if packVersion != version {
|
|
fmt.Println("Set " + core.ComponentToFriendlyName(component) + " version to " + version + " (previously " + packVersion + ")")
|
|
}
|
|
pack.Versions[component] = version
|
|
}
|
|
}
|
|
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.ProjectID
|
|
}
|
|
|
|
fmt.Println("Querying Curse API for mod info...")
|
|
|
|
modInfos, err := cfDefaultClient.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????
|
|
|
|
modFileInfosMap := make(map[int]modFileInfo)
|
|
referencedModPaths := make([]string, 0, len(modsList))
|
|
successes := 0
|
|
remainingFileIDs := make([]int, 0, len(modsList))
|
|
|
|
// 1st pass: query mod metadata for every CurseForge file
|
|
for _, v := range modsList {
|
|
modInfoValue, ok := modInfosMap[v.ProjectID]
|
|
if !ok {
|
|
fmt.Printf("Failed to obtain mod information for addon/file IDs %d/%d\n", v.ProjectID, v.FileID)
|
|
continue
|
|
}
|
|
|
|
found := false
|
|
var fileInfo modFileInfo
|
|
for _, fileInfo = range modInfoValue.LatestFiles {
|
|
if fileInfo.ID == v.FileID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if found {
|
|
modFileInfosMap[v.FileID] = fileInfo
|
|
} else {
|
|
remainingFileIDs = append(remainingFileIDs, v.FileID)
|
|
}
|
|
}
|
|
|
|
// 2nd pass: query files that weren't in the previous results
|
|
fmt.Println("Querying Curse API for file info...")
|
|
|
|
modFileInfos, err := cfDefaultClient.getFileInfoMultiple(remainingFileIDs)
|
|
if err != nil {
|
|
fmt.Printf("Failed to obtain mod file information: %s\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
for _, v := range modFileInfos {
|
|
modFileInfosMap[v.ID] = v
|
|
}
|
|
|
|
// 3rd pass: create mod files for every file
|
|
for _, v := range modsList {
|
|
modInfoValue, ok := modInfosMap[v.ProjectID]
|
|
if !ok {
|
|
fmt.Printf("Failed to obtain mod information for addon/file IDs %d/%d\n", v.ProjectID, v.FileID)
|
|
continue
|
|
}
|
|
|
|
modFileInfoValue, ok := modFileInfosMap[v.FileID]
|
|
if !ok {
|
|
fmt.Printf("Failed to obtain mod file information for addon/file IDs %d/%d\n", v.ProjectID, v.FileID)
|
|
continue
|
|
}
|
|
|
|
err = createModFile(modInfoValue, modFileInfoValue, &index, v.OptionalDisabled)
|
|
if err != nil {
|
|
fmt.Printf("Failed to save mod \"%s\": %s\n", modInfoValue.Name, err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
modFilePath := getPathForFile(modInfoValue.GameID, modInfoValue.ClassID, modInfoValue.PrimaryCategoryID, modInfoValue.Slug)
|
|
ref, err := filepath.Abs(filepath.Join(filepath.Dir(modFilePath), modFileInfoValue.FileName))
|
|
if err == nil {
|
|
referencedModPaths = append(referencedModPaths, ref)
|
|
}
|
|
|
|
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
|
|
packRoot := index.GetPackRoot()
|
|
for _, v := range filesList {
|
|
filePath := filepath.Join(packRoot, filepath.FromSlash(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 v.Name() == "manifest.json" || v.Name() == "minecraftinstance.json" || v.Name() == ".curseclient" {
|
|
fmt.Printf("Ignored file \"%s\"\n", v.Name())
|
|
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)
|
|
}
|