mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 21:16:30 +02:00
305 lines
8.0 KiB
Go
305 lines
8.0 KiB
Go
package modrinth
|
|
|
|
import (
|
|
"archive/zip"
|
|
"encoding/json"
|
|
"fmt"
|
|
"github.com/packwiz/packwiz/cmdshared"
|
|
"github.com/spf13/viper"
|
|
"golang.org/x/exp/slices"
|
|
"net/url"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
|
|
"github.com/packwiz/packwiz/core"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
// exportCmd represents the export command
|
|
var exportCmd = &cobra.Command{
|
|
Use: "export",
|
|
Short: "Export the current modpack into a .mrpack for Modrinth",
|
|
Args: cobra.NoArgs,
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
fmt.Println("Loading modpack...")
|
|
pack, err := core.LoadPack()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
index, err := pack.LoadIndex()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
os.Exit(1)
|
|
}
|
|
// Do a refresh to ensure files are up to date
|
|
err = index.Refresh()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
err = index.Write()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
err = pack.UpdateIndexHash()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
err = pack.Write()
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
// TODO: should index just expose indexPath itself, through a function?
|
|
indexPath := filepath.Join(filepath.Dir(viper.GetString("pack-file")), filepath.FromSlash(pack.Index.File))
|
|
|
|
fmt.Println("Reading external files...")
|
|
mods, err := index.LoadAllMods()
|
|
if err != nil {
|
|
fmt.Printf("Error reading file: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
fileName := viper.GetString("modrinth.export.output")
|
|
if fileName == "" {
|
|
fileName = pack.GetPackName() + ".mrpack"
|
|
}
|
|
expFile, err := os.Create(fileName)
|
|
if err != nil {
|
|
fmt.Printf("Failed to create zip: %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
exp := zip.NewWriter(expFile)
|
|
|
|
// Add an overrides folder even if there are no files to go in it
|
|
_, err = exp.Create("overrides/")
|
|
if err != nil {
|
|
fmt.Printf("Failed to add overrides folder: %s\n", err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Printf("Retrieving %v external files...\n", len(mods))
|
|
|
|
restrictDomains := viper.GetBool("modrinth.export.restrictDomains")
|
|
|
|
for _, mod := range mods {
|
|
if !canBeIncludedDirectly(mod, restrictDomains) {
|
|
cmdshared.PrintDisclaimer(false)
|
|
break
|
|
}
|
|
}
|
|
|
|
session, err := core.CreateDownloadSession(mods, []string{"sha1", "sha512", "length-bytes"})
|
|
if err != nil {
|
|
fmt.Printf("Error retrieving external files: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
cmdshared.ListManualDownloads(session)
|
|
|
|
manifestFiles := make([]PackFile, 0)
|
|
for dl := range session.StartDownloads() {
|
|
if canBeIncludedDirectly(dl.Mod, restrictDomains) {
|
|
if dl.Error != nil {
|
|
fmt.Printf("Download of %s (%s) failed: %v\n", dl.Mod.Name, dl.Mod.FileName, dl.Error)
|
|
continue
|
|
}
|
|
for warning := range dl.Warnings {
|
|
fmt.Printf("Warning for %s (%s): %v\n", dl.Mod.Name, dl.Mod.FileName, warning)
|
|
}
|
|
|
|
pathForward, err := filepath.Rel(filepath.Dir(indexPath), dl.Mod.GetDestFilePath())
|
|
if err != nil {
|
|
fmt.Printf("Error resolving mod file: %s\n", err.Error())
|
|
// TODO: exit(1)?
|
|
continue
|
|
}
|
|
|
|
path := filepath.ToSlash(pathForward)
|
|
|
|
hashes := make(map[string]string)
|
|
hashes["sha1"] = dl.Hashes["sha1"]
|
|
hashes["sha512"] = dl.Hashes["sha512"]
|
|
fileSize, err := strconv.ParseUint(dl.Hashes["length-bytes"], 10, 64)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
// Create env options based on configured optional/side
|
|
var envInstalled string
|
|
if dl.Mod.Option != nil && dl.Mod.Option.Optional {
|
|
envInstalled = "optional"
|
|
} else {
|
|
envInstalled = "required"
|
|
}
|
|
var clientEnv, serverEnv string
|
|
if dl.Mod.Side == core.UniversalSide || dl.Mod.Side == core.EmptySide {
|
|
clientEnv = envInstalled
|
|
serverEnv = envInstalled
|
|
} else if dl.Mod.Side == core.ClientSide {
|
|
clientEnv = envInstalled
|
|
serverEnv = "unsupported"
|
|
} else if dl.Mod.Side == core.ServerSide {
|
|
clientEnv = "unsupported"
|
|
serverEnv = envInstalled
|
|
}
|
|
|
|
// Modrinth URLs must be RFC3986
|
|
u, err := core.ReencodeURL(dl.Mod.Download.URL)
|
|
if err != nil {
|
|
fmt.Printf("Error re-encoding mod URL: %s\n", err.Error())
|
|
u = dl.Mod.Download.URL
|
|
}
|
|
|
|
manifestFiles = append(manifestFiles, PackFile{
|
|
Path: path,
|
|
Hashes: hashes,
|
|
Env: &struct {
|
|
Client string `json:"client"`
|
|
Server string `json:"server"`
|
|
}{Client: clientEnv, Server: serverEnv},
|
|
Downloads: []string{u},
|
|
FileSize: uint32(fileSize),
|
|
})
|
|
|
|
fmt.Printf("%s (%s) added to manifest\n", dl.Mod.Name, dl.Mod.FileName)
|
|
} else {
|
|
if dl.Mod.Side == core.ClientSide {
|
|
_ = cmdshared.AddToZip(dl, exp, "client-overrides", indexPath)
|
|
} else if dl.Mod.Side == core.ServerSide {
|
|
_ = cmdshared.AddToZip(dl, exp, "server-overrides", indexPath)
|
|
} else {
|
|
_ = cmdshared.AddToZip(dl, exp, "overrides", indexPath)
|
|
}
|
|
}
|
|
}
|
|
|
|
err = session.SaveIndex()
|
|
if err != nil {
|
|
fmt.Printf("Error saving cache index: %v\n", err)
|
|
os.Exit(1)
|
|
}
|
|
|
|
dependencies := make(map[string]string)
|
|
dependencies["minecraft"], err = pack.GetMCVersion()
|
|
if err != nil {
|
|
_ = exp.Close()
|
|
_ = expFile.Close()
|
|
fmt.Println("Error creating manifest: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
if quiltVersion, ok := pack.Versions["quilt"]; ok {
|
|
dependencies["quilt-loader"] = quiltVersion
|
|
} else if fabricVersion, ok := pack.Versions["fabric"]; ok {
|
|
dependencies["fabric-loader"] = fabricVersion
|
|
} else if forgeVersion, ok := pack.Versions["forge"]; ok {
|
|
dependencies["forge"] = forgeVersion
|
|
}
|
|
|
|
manifest := Pack{
|
|
FormatVersion: 1,
|
|
Game: "minecraft",
|
|
VersionID: pack.Version,
|
|
Name: pack.Name,
|
|
Summary: pack.Description,
|
|
Files: manifestFiles,
|
|
Dependencies: dependencies,
|
|
}
|
|
|
|
if len(pack.Version) == 0 {
|
|
fmt.Println("Warning: pack.toml version field must not be empty to create a valid Modrinth pack")
|
|
}
|
|
|
|
manifestFile, err := exp.Create("modrinth.index.json")
|
|
if err != nil {
|
|
_ = exp.Close()
|
|
_ = expFile.Close()
|
|
fmt.Println("Error creating manifest: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
w := json.NewEncoder(manifestFile)
|
|
w.SetIndent("", " ") // Documentation uses 4 spaces
|
|
err = w.Encode(manifest)
|
|
if err != nil {
|
|
_ = exp.Close()
|
|
_ = expFile.Close()
|
|
fmt.Println("Error writing manifest: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
|
|
err = exp.Close()
|
|
if err != nil {
|
|
fmt.Println("Error writing export file: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
err = expFile.Close()
|
|
if err != nil {
|
|
fmt.Println("Error writing export file: " + err.Error())
|
|
os.Exit(1)
|
|
}
|
|
|
|
fmt.Println("Modpack exported to " + fileName)
|
|
},
|
|
}
|
|
|
|
var whitelistedHosts = []string{
|
|
"cdn.modrinth.com",
|
|
"github.com",
|
|
"raw.githubusercontent.com",
|
|
"gitlab.com",
|
|
}
|
|
|
|
func canBeIncludedDirectly(mod *core.Mod, restrictDomains bool) bool {
|
|
if mod.Download.Mode == "url" || mod.Download.Mode == "" {
|
|
if !restrictDomains {
|
|
return true
|
|
}
|
|
|
|
modUrl, err := url.Parse(mod.Download.URL)
|
|
if err == nil {
|
|
if slices.Contains(whitelistedHosts, modUrl.Host) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func init() {
|
|
modrinthCmd.AddCommand(exportCmd)
|
|
exportCmd.Flags().Bool("restrictDomains", true, "Restricts domains to those allowed by modrinth.com")
|
|
exportCmd.Flags().StringP("output", "o", "", "The file to export the modpack to")
|
|
_ = viper.BindPFlag("modrinth.export.restrictDomains", exportCmd.Flags().Lookup("restrictDomains"))
|
|
_ = viper.BindPFlag("modrinth.export.output", exportCmd.Flags().Lookup("output"))
|
|
}
|