packwiz/core/mod.go
comp500 0f3096e251 Use the correct directories for non-mod files; use .pw.toml extension
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.
2022-05-16 21:06:10 +01:00

160 lines
4.2 KiB
Go

package core
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"github.com/BurntSushi/toml"
)
// Mod stores metadata about a mod. This is written to a TOML file for each mod.
type Mod struct {
metaFile string // The file for the metadata file, used as an ID
Name string `toml:"name"`
FileName string `toml:"filename"`
Side string `toml:"side,omitempty"`
Download ModDownload `toml:"download"`
// Update is a map of map of stuff, so you can store arbitrary values on string keys to define updating
Update map[string]map[string]interface{} `toml:"update"`
updateData map[string]interface{}
Option *ModOption `toml:"option,omitempty"`
}
// ModDownload specifies how to download the mod file
type ModDownload struct {
URL string `toml:"url"`
HashFormat string `toml:"hash-format"`
Hash string `toml:"hash"`
}
// ModOption specifies optional metadata for this mod file
type ModOption struct {
Optional bool `toml:"optional"`
Description string `toml:"description,omitempty"`
Default bool `toml:"default,omitempty"`
}
// The three possible values of Side (the side that the mod is on) are "server", "client", and "both".
//noinspection GoUnusedConst
const (
ServerSide = "server"
ClientSide = "client"
UniversalSide = "both"
)
// LoadMod attempts to load a mod file from a path
func LoadMod(modFile string) (Mod, error) {
var mod Mod
if _, err := toml.DecodeFile(modFile, &mod); err != nil {
return Mod{}, err
}
mod.updateData = make(map[string]interface{})
// Horrible reflection library to convert map[string]interface to proper struct
for k, v := range mod.Update {
updater, ok := Updaters[k]
if ok {
updateData, err := updater.ParseUpdate(v)
if err != nil {
return mod, err
}
mod.updateData[k] = updateData
} else {
return mod, errors.New("Update plugin " + k + " not found!")
}
}
mod.metaFile = modFile
return mod, nil
}
// SetMetaPath sets the file path of a metadata file
func (m *Mod) SetMetaPath(metaFile string) string {
m.metaFile = metaFile
return m.metaFile
}
// Write saves the mod file, returning a hash format and the value of the hash of the saved file
func (m Mod) Write() (string, string, error) {
f, err := os.Create(m.metaFile)
if err != nil {
// Attempt to create the containing directory
err2 := os.MkdirAll(filepath.Dir(m.metaFile), os.ModePerm)
if err2 == nil {
f, err = os.Create(m.metaFile)
}
if err != nil {
return "sha256", "", err
}
}
h, stringer, err := GetHashImpl("sha256")
if err != nil {
_ = f.Close()
return "", "", err
}
w := io.MultiWriter(h, f)
enc := toml.NewEncoder(w)
// Disable indentation
enc.Indent = ""
err = enc.Encode(m)
hashString := stringer.HashToString(h.Sum(nil))
if err != nil {
_ = f.Close()
return "sha256", hashString, err
}
return "sha256", hashString, f.Close()
}
// GetParsedUpdateData can be used to retrieve updater-specific information after parsing a mod file
func (m Mod) GetParsedUpdateData(updaterName string) (interface{}, bool) {
upd, ok := m.updateData[updaterName]
return upd, ok
}
// GetFilePath is a clumsy hack that I made because Mod already stores it's path anyway
func (m Mod) GetFilePath() string {
return m.metaFile
}
// GetDestFilePath returns the path of the destination file of the mod
func (m Mod) GetDestFilePath() string {
return filepath.Join(filepath.Dir(m.metaFile), filepath.FromSlash(m.FileName))
}
// DownloadFile attempts to resolve and download the file
func (m Mod) DownloadFile(dest io.Writer) error {
resp, err := http.Get(m.Download.URL)
if err != nil {
return err
}
if resp.StatusCode != 200 {
_ = resp.Body.Close()
return errors.New("invalid status code " + strconv.Itoa(resp.StatusCode))
}
h, stringer, err := GetHashImpl(m.Download.HashFormat)
if err != nil {
return err
}
w := io.MultiWriter(h, dest)
_, err = io.Copy(w, resp.Body)
if err != nil {
return err
}
calculatedHash := stringer.HashToString(h.Sum(nil))
// Check if the hash of the downloaded file matches the expected hash.
if calculatedHash != m.Download.Hash {
return fmt.Errorf("Hash of downloaded file does not match with expected hash!\n download hash: %s\n expected hash: %s\n", calculatedHash, m.Download.Hash)
}
return nil
}