mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 13:06:30 +02:00
* Update list of Modrinth approved domains * Add flag to disable Modrinth restricted domains This allows packwiz to produce .mrpack files with direct downloads, for packs that aren't distributed on modrinth.com.
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 {
|
|
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"))
|
|
}
|