Implement pack importing/exporting for downloaded Curseforge packs

Abstract out hash implementations
Implement file saving/downloading
This commit is contained in:
comp500
2019-11-12 22:11:40 +00:00
parent 73f6184b3d
commit 5dfe23e51d
12 changed files with 749 additions and 297 deletions

View File

@@ -0,0 +1,81 @@
package packinterop
import (
"bufio"
"io"
"io/ioutil"
"os"
"path/filepath"
)
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)
}
type readerFile struct {
NameInternal string
Reader *io.ReadCloser
}
func (f readerFile) Name() string {
return f.NameInternal
}
func (f readerFile) Open() (io.ReadCloser, error) {
return *f.Reader, nil
}
type diskPackSource struct {
MetaSource *bufio.Reader
MetaName string
BasePath string
}
func (s diskPackSource) GetFile(path string) (ImportPackFile, error) {
return diskFile{path, filepath.Join(s.BasePath, filepath.FromSlash(path))}, nil
}
func (s diskPackSource) GetFileList() ([]ImportPackFile, error) {
list := make([]ImportPackFile, 0)
err := filepath.Walk(s.BasePath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil
}
// Get the name of the file, relative to the pack folder
name, err := filepath.Rel(s.BasePath, path)
if err != nil {
return err
}
list = append(list, diskFile{filepath.ToSlash(name), path})
return nil
})
if err != nil {
return nil, err
}
return list, nil
}
func (s diskPackSource) GetPackFile() ImportPackFile {
rc := ioutil.NopCloser(s.MetaSource)
return readerFile{s.MetaName, &rc}
}
func GetDiskPackSource(metaSource *bufio.Reader, metaName string, basePath string) ImportPackSource {
return diskPackSource{
MetaSource: metaSource,
MetaName: metaName,
BasePath: basePath,
}
}

View File

@@ -0,0 +1,25 @@
package packinterop
import "io"
type ImportPackFile interface {
Name() string
Open() (io.ReadCloser, error)
}
type ImportPackMetadata interface {
Name() string
Versions() map[string]string
// TODO: use AddonFileReference?
Mods() []struct {
ModID int
FileID int
}
GetFiles() ([]ImportPackFile, error)
}
type ImportPackSource interface {
GetFile(path string) (ImportPackFile, error)
GetFileList() ([]ImportPackFile, error)
GetPackFile() ImportPackFile
}

View File

@@ -0,0 +1,104 @@
package packinterop
import "strings"
type cursePackMeta struct {
Minecraft struct {
Version string `json:"version"`
ModLoaders []modLoaderDef `json:"modLoaders"`
} `json:"minecraft"`
ManifestType string `json:"manifestType"`
ManifestVersion int `json:"manifestVersion"`
NameInternal string `json:"name"`
Version string `json:"version"`
Author string `json:"author"`
ProjectID int `json:"projectID"`
Files []struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
} `json:"files"`
Overrides string `json:"overrides"`
importSrc ImportPackSource
}
type modLoaderDef struct {
ID string `json:"id"`
Primary bool `json:"primary"`
}
func (c cursePackMeta) Name() string {
return c.NameInternal
}
func (c cursePackMeta) Versions() map[string]string {
vers := make(map[string]string)
vers["minecraft"] = c.Minecraft.Version
for _, v := range c.Minecraft.ModLoaders {
// Seperate dash-separated modloader/version pairs
parts := strings.SplitN(v.ID, "-", 2)
if len(parts) == 2 {
vers[parts[0]] = parts[1]
}
}
if val, ok := vers["forge"]; ok {
// Remove the minecraft version prefix, if it exists
vers["forge"] = strings.TrimPrefix(val, c.Minecraft.Version+"-")
}
return vers
}
func (c cursePackMeta) Mods() []struct {
ModID int
FileID int
} {
list := make([]struct {
ModID int
FileID int
}, len(c.Files))
for i, v := range c.Files {
list[i] = struct {
ModID int
FileID int
}{
ModID: v.ProjectID,
FileID: v.FileID,
}
}
return list
}
type cursePackOverrideWrapper struct {
name string
ImportPackFile
}
func (w cursePackOverrideWrapper) Name() string {
return w.name
}
func (c cursePackMeta) GetFiles() ([]ImportPackFile, error) {
// Only import files from overrides directory
if len(c.Overrides) > 0 {
fullList, err := c.importSrc.GetFileList()
if err != nil {
return nil, err
}
overridesList := make([]ImportPackFile, 0, len(fullList))
overridesPath := c.Overrides
if !strings.HasSuffix(overridesPath, "/") {
overridesPath = c.Overrides + "/"
}
// Wrap files, removing overrides/ from the start
for _, v := range fullList {
if strings.HasPrefix(v.Name(), overridesPath) {
overridesList = append(overridesList, cursePackOverrideWrapper{
name: strings.TrimPrefix(v.Name(), overridesPath),
ImportPackFile: v,
})
}
}
return overridesList, nil
}
return []ImportPackFile{}, nil
}

View File

@@ -0,0 +1,85 @@
package packinterop
import (
"path/filepath"
"strings"
)
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"`
importSrc ImportPackSource
}
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"] = strings.TrimPrefix(m.Modloader.Name, "forge-")
}
// Remove the minecraft version prefix, if it exists
vers["forge"] = strings.TrimPrefix(vers["forge"], m.MCVersion+"-")
}
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) {
// If the modpack is unlocked, import all the files
// Otherwise import just the modpack overrides
if m.IsUnlocked {
return m.importSrc.GetFileList()
}
list := make([]ImportPackFile, len(m.ModpackOverrides))
var err error
for i, v := range m.ModpackOverrides {
list[i], err = m.importSrc.GetFile(filepath.ToSlash(v))
if err != nil {
return nil, err
}
}
return list, nil
}

View File

@@ -0,0 +1,119 @@
package packinterop
import (
"bytes"
"encoding/json"
"fmt"
"github.com/comp500/packwiz/core"
"io"
"io/ioutil"
"os"
)
func ReadMetadata(s ImportPackSource) ImportPackMetadata {
var packImport ImportPackMetadata
metaFile := s.GetPackFile()
rdr, err := metaFile.Open()
if err != nil {
fmt.Printf("Error reading file: %s\n", err)
os.Exit(1)
}
// Read the whole file (as we are going to parse it multiple times)
fileData, err := ioutil.ReadAll(rdr)
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 {
packMeta := cursePackMeta{importSrc: s}
err = json.Unmarshal(fileData, &packMeta)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
packImport = packMeta
} else {
// Replace FileNameOnDisk with fileNameOnDisk
fileData = bytes.ReplaceAll(fileData, []byte("FileNameOnDisk"), []byte("fileNameOnDisk"))
packMeta := twitchInstalledPackMeta{importSrc: s}
err = json.Unmarshal(fileData, &packMeta)
if err != nil {
fmt.Printf("Error parsing JSON: %s\n", err)
os.Exit(1)
}
packImport = packMeta
}
return packImport
}
// AddonFileReference is a pair of Project ID and File ID to reference a single file on CurseForge
type AddonFileReference struct {
ProjectID int
FileID int
}
func WriteManifestFromPack(pack core.Pack, fileRefs []AddonFileReference, out io.Writer) error {
// TODO: should Required be false sometimes?
files := make([]struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
}, len(fileRefs))
for i, fr := range fileRefs {
files[i] = struct {
ProjectID int `json:"projectID"`
FileID int `json:"fileID"`
Required bool `json:"required"`
}{ProjectID: fr.ProjectID, FileID: fr.FileID, Required: true}
}
modLoaders := make([]modLoaderDef, 0, 1)
forgeVersion, ok := pack.Versions["forge"]
if ok {
modLoaders = append(modLoaders, modLoaderDef{
ID: "forge-" + forgeVersion,
Primary: true,
})
}
manifest := cursePackMeta{
Minecraft: struct {
Version string `json:"version"`
ModLoaders []modLoaderDef `json:"modLoaders"`
}{
Version: pack.Versions["minecraft"],
ModLoaders: modLoaders,
},
ManifestType: "minecraftModpack",
ManifestVersion: 1,
NameInternal: pack.Name,
Version: "", // TODO: store or take this?
Author: "", // TODO: store or take this?
ProjectID: 0, // TODO: store or take this?
Files: files,
Overrides: "overrides",
}
w := json.NewEncoder(out)
w.SetIndent("", " ") // Match CF export
err := w.Encode(manifest)
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,57 @@
package packinterop
import (
"archive/zip"
"errors"
)
type zipReaderFile struct {
NameInternal string
*zip.File
}
func (f zipReaderFile) Name() string {
return f.NameInternal
}
type zipPackSource struct {
MetaFile *zip.File
Reader *zip.Reader
cachedFileList []ImportPackFile
}
func (s zipPackSource) GetFile(path string) (ImportPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]ImportPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
for _, v := range s.cachedFileList {
if v.Name() == path {
return v, nil
}
}
return zipReaderFile{}, errors.New("file not found in zip")
}
func (s zipPackSource) GetFileList() ([]ImportPackFile, error) {
if s.cachedFileList == nil {
s.cachedFileList = make([]ImportPackFile, len(s.Reader.File))
for i, v := range s.Reader.File {
s.cachedFileList[i] = zipReaderFile{v.Name, v}
}
}
return s.cachedFileList, nil
}
func (s zipPackSource) GetPackFile() ImportPackFile {
return zipReaderFile{s.MetaFile.Name, s.MetaFile}
}
func GetZipPackSource(metaFile *zip.File, reader *zip.Reader) ImportPackSource {
return zipPackSource{
MetaFile: metaFile,
Reader: reader,
}
}