mirror of
				https://github.com/packwiz/packwiz.git
				synced 2025-11-04 12:04:31 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			369 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			369 lines
		
	
	
		
			9.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package core
 | 
						|
 | 
						|
import (
 | 
						|
	"encoding/hex"
 | 
						|
	"errors"
 | 
						|
	"io"
 | 
						|
	"os"
 | 
						|
	"path/filepath"
 | 
						|
	"sort"
 | 
						|
	"strings"
 | 
						|
	"time"
 | 
						|
 | 
						|
	"github.com/BurntSushi/toml"
 | 
						|
	"github.com/denormal/go-gitignore"
 | 
						|
	"github.com/spf13/viper"
 | 
						|
	"github.com/vbauerster/mpb/v4"
 | 
						|
	"github.com/vbauerster/mpb/v4/decor"
 | 
						|
)
 | 
						|
 | 
						|
// Index is a representation of the index.toml file for referencing all the files in a pack.
 | 
						|
type Index struct {
 | 
						|
	HashFormat string      `toml:"hash-format"`
 | 
						|
	Files      []IndexFile `toml:"files"`
 | 
						|
	indexFile  string
 | 
						|
}
 | 
						|
 | 
						|
// IndexFile is a file in the index
 | 
						|
type IndexFile struct {
 | 
						|
	// Files are stored in forward-slash format relative to the index file
 | 
						|
	File           string `toml:"file"`
 | 
						|
	Hash           string `toml:"hash,omitempty"`
 | 
						|
	HashFormat     string `toml:"hash-format,omitempty"`
 | 
						|
	Alias          string `toml:"alias,omitempty"`
 | 
						|
	MetaFile       bool   `toml:"metafile,omitempty"` // True when it is a .toml metadata file
 | 
						|
	Preserve       bool   `toml:"preserve,omitempty"` // Don't overwrite the file when updating
 | 
						|
	fileExistsTemp bool
 | 
						|
}
 | 
						|
 | 
						|
// LoadIndex attempts to load the index file from a path
 | 
						|
func LoadIndex(indexFile string) (Index, error) {
 | 
						|
	var index Index
 | 
						|
	if _, err := toml.DecodeFile(indexFile, &index); err != nil {
 | 
						|
		return Index{}, err
 | 
						|
	}
 | 
						|
	index.indexFile = indexFile
 | 
						|
	if len(index.HashFormat) == 0 {
 | 
						|
		index.HashFormat = "sha256"
 | 
						|
	}
 | 
						|
	return index, nil
 | 
						|
}
 | 
						|
 | 
						|
// RemoveFile removes a file from the index.
 | 
						|
func (in *Index) RemoveFile(path string) error {
 | 
						|
	relPath, err := filepath.Rel(filepath.Dir(in.indexFile), path)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	i := 0
 | 
						|
	for _, file := range in.Files {
 | 
						|
		if filepath.Clean(filepath.FromSlash(file.File)) != relPath {
 | 
						|
			// Keep file, as it doesn't match
 | 
						|
			in.Files[i] = file
 | 
						|
			i++
 | 
						|
		}
 | 
						|
	}
 | 
						|
	in.Files = in.Files[:i]
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// resortIndex sorts Files by file name
 | 
						|
func (in *Index) resortIndex() {
 | 
						|
	sort.SliceStable(in.Files, func(i, j int) bool {
 | 
						|
		// TODO: Compare by alias if names are equal?
 | 
						|
		// TODO: Remove duplicated entries? (compound key on file/alias?)
 | 
						|
		return in.Files[i].File < in.Files[j].File
 | 
						|
	})
 | 
						|
}
 | 
						|
 | 
						|
func (in *Index) updateFileHashGiven(path, format, hash string, mod bool) error {
 | 
						|
	// Find in index
 | 
						|
	found := false
 | 
						|
	relPath, err := filepath.Rel(filepath.Dir(in.indexFile), path)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	for k, v := range in.Files {
 | 
						|
		if filepath.Clean(filepath.FromSlash(v.File)) == relPath {
 | 
						|
			found = true
 | 
						|
			// Update hash
 | 
						|
			in.Files[k].Hash = hash
 | 
						|
			if in.HashFormat == format {
 | 
						|
				in.Files[k].HashFormat = ""
 | 
						|
			} else {
 | 
						|
				in.Files[k].HashFormat = format
 | 
						|
			}
 | 
						|
			// Mark this file as found
 | 
						|
			in.Files[k].fileExistsTemp = true
 | 
						|
			// Clean up path if it's untidy
 | 
						|
			in.Files[k].File = filepath.ToSlash(relPath)
 | 
						|
			// Don't break out of loop, as there may be aliased versions that
 | 
						|
			// also need to be updated
 | 
						|
		}
 | 
						|
	}
 | 
						|
	if !found {
 | 
						|
		newFile := IndexFile{
 | 
						|
			File:           filepath.ToSlash(relPath),
 | 
						|
			Hash:           hash,
 | 
						|
			fileExistsTemp: true,
 | 
						|
		}
 | 
						|
		// Override hash format for this file, if the whole index isn't sha256
 | 
						|
		if in.HashFormat != format {
 | 
						|
			newFile.HashFormat = format
 | 
						|
		}
 | 
						|
		newFile.MetaFile = mod
 | 
						|
 | 
						|
		in.Files = append(in.Files, newFile)
 | 
						|
	}
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// updateFile calculates the hash for a given path and updates it in the index
 | 
						|
func (in *Index) updateFile(path string) error {
 | 
						|
	var hashString string
 | 
						|
	if viper.GetBool("no-internal-hashes") {
 | 
						|
		hashString = ""
 | 
						|
	} else {
 | 
						|
		f, err := os.Open(path)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Hash usage strategy (may change):
 | 
						|
		// Just use SHA256, overwrite existing hash regardless of what it is
 | 
						|
		// May update later to continue using the same hash that was already being used
 | 
						|
		h, err := GetHashImpl("sha256")
 | 
						|
		if err != nil {
 | 
						|
			_ = f.Close()
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		if _, err := io.Copy(h, f); err != nil {
 | 
						|
			_ = f.Close()
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		err = f.Close()
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
		hashString = hex.EncodeToString(h.Sum(nil))
 | 
						|
	}
 | 
						|
 | 
						|
	mod := false
 | 
						|
	// If the file has an extension of toml and is in the mods folder, set mod to true
 | 
						|
	absFileDir, err := filepath.Abs(filepath.Dir(path))
 | 
						|
	if err == nil {
 | 
						|
		modsDir := filepath.Join(in.GetPackRoot(), viper.GetString("mods-folder"))
 | 
						|
		absModsDir, err := filepath.Abs(modsDir)
 | 
						|
		if err == nil {
 | 
						|
			if absFileDir == absModsDir && strings.HasSuffix(filepath.Base(path), ".toml") {
 | 
						|
				mod = true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
 | 
						|
	return in.updateFileHashGiven(path, "sha256", hashString, mod)
 | 
						|
}
 | 
						|
 | 
						|
func (in Index) GetPackRoot() string {
 | 
						|
	return filepath.Dir(in.indexFile)
 | 
						|
}
 | 
						|
 | 
						|
// Refresh updates the hashes of all the files in the index, and adds new files to the index
 | 
						|
func (in *Index) Refresh() error {
 | 
						|
	// TODO: If needed, multithreaded hashing
 | 
						|
	// for i := 0; i < runtime.NumCPU(); i++ {}
 | 
						|
 | 
						|
	// Is case-sensitivity a problem?
 | 
						|
	pathPF, _ := filepath.Abs(viper.GetString("pack-file"))
 | 
						|
	pathIndex, _ := filepath.Abs(in.indexFile)
 | 
						|
 | 
						|
	packRoot := in.GetPackRoot()
 | 
						|
	ignoreExists := true
 | 
						|
	pathIgnore, _ := filepath.Abs(filepath.Join(packRoot, ".packwizignore"))
 | 
						|
	ignore, err := gitignore.NewFromFile(filepath.Join(packRoot, ".packwizignore"))
 | 
						|
	if err != nil {
 | 
						|
		ignoreExists = false
 | 
						|
	}
 | 
						|
 | 
						|
	var fileList []string
 | 
						|
	err = filepath.Walk(packRoot, func(path string, info os.FileInfo, err error) error {
 | 
						|
		if err != nil {
 | 
						|
			// TODO: Handle errors on individual files properly
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		// Exit if the files are the same as the pack/index files
 | 
						|
		absPath, _ := filepath.Abs(path)
 | 
						|
		if absPath == pathPF || absPath == pathIndex {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		// Exit if this is a directory
 | 
						|
		if info.IsDir() {
 | 
						|
			return nil
 | 
						|
		}
 | 
						|
		if ignoreExists {
 | 
						|
			if absPath == pathIgnore {
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
 | 
						|
			if ignore.Ignore(absPath) {
 | 
						|
				return nil
 | 
						|
			}
 | 
						|
		}
 | 
						|
 | 
						|
		fileList = append(fileList, path)
 | 
						|
		return nil
 | 
						|
	})
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	progressContainer := mpb.New()
 | 
						|
	progress := progressContainer.AddBar(int64(len(fileList)),
 | 
						|
		mpb.PrependDecorators(
 | 
						|
			// simple name decorator
 | 
						|
			decor.Name("Refreshing index..."),
 | 
						|
			// decor.DSyncWidth bit enables column width synchronization
 | 
						|
			decor.Percentage(decor.WCSyncSpace),
 | 
						|
		),
 | 
						|
		mpb.AppendDecorators(
 | 
						|
			// replace ETA decorator with "done" message, OnComplete event
 | 
						|
			decor.OnComplete(
 | 
						|
				// ETA decorator with ewma age of 60
 | 
						|
				decor.EwmaETA(decor.ET_STYLE_GO, 60), "done",
 | 
						|
			),
 | 
						|
		),
 | 
						|
	)
 | 
						|
 | 
						|
	for _, v := range fileList {
 | 
						|
		start := time.Now()
 | 
						|
 | 
						|
		err := in.updateFile(v)
 | 
						|
		if err != nil {
 | 
						|
			return err
 | 
						|
		}
 | 
						|
 | 
						|
		progress.Increment(time.Since(start))
 | 
						|
	}
 | 
						|
	// Close bar
 | 
						|
	progress.SetTotal(int64(len(fileList)), true) // If len = 0, we have to manually set complete to true
 | 
						|
	progressContainer.Wait()
 | 
						|
 | 
						|
	// Check all the files exist, remove them if they don't
 | 
						|
	i := 0
 | 
						|
	for _, file := range in.Files {
 | 
						|
		if file.fileExistsTemp {
 | 
						|
			// Keep file if it exists (already checked in updateFile)
 | 
						|
			in.Files[i] = file
 | 
						|
			i++
 | 
						|
		}
 | 
						|
	}
 | 
						|
	in.Files = in.Files[:i]
 | 
						|
 | 
						|
	in.resortIndex()
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// RefreshFile calculates the hash for a given path and updates it in the index (also sorts the index)
 | 
						|
func (in *Index) RefreshFile(path string) error {
 | 
						|
	err := in.updateFile(path)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	in.resortIndex()
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// Write saves the index file
 | 
						|
func (in Index) Write() error {
 | 
						|
	// TODO: calculate and provide hash while writing?
 | 
						|
	f, err := os.Create(in.indexFile)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	enc := toml.NewEncoder(f)
 | 
						|
	// Disable indentation
 | 
						|
	enc.Indent = ""
 | 
						|
	err = enc.Encode(in)
 | 
						|
	if err != nil {
 | 
						|
		_ = f.Close()
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	return f.Close()
 | 
						|
}
 | 
						|
 | 
						|
// RefreshFileWithHash updates a file in the index, given a file hash and whether it is a mod or not
 | 
						|
func (in *Index) RefreshFileWithHash(path, format, hash string, mod bool) error {
 | 
						|
	if viper.GetBool("no-internal-hashes") {
 | 
						|
		hash = ""
 | 
						|
	}
 | 
						|
	err := in.updateFileHashGiven(path, format, hash, mod)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	in.resortIndex()
 | 
						|
	return nil
 | 
						|
}
 | 
						|
 | 
						|
// FindMod finds a mod in the index and returns it's path and whether it has been found
 | 
						|
func (in Index) FindMod(modName string) (string, bool) {
 | 
						|
	for _, v := range in.Files {
 | 
						|
		if v.MetaFile {
 | 
						|
			_, file := filepath.Split(v.File)
 | 
						|
			fileTrimmed := strings.TrimSuffix(file, ModExtension)
 | 
						|
			if fileTrimmed == modName {
 | 
						|
				return filepath.Join(filepath.Dir(in.indexFile), filepath.FromSlash(v.File)), true
 | 
						|
			}
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return "", false
 | 
						|
}
 | 
						|
 | 
						|
// GetAllMods finds paths to every metadata file (Mod) in the index
 | 
						|
func (in Index) GetAllMods() []string {
 | 
						|
	var list []string
 | 
						|
	baseDir := filepath.Dir(in.indexFile)
 | 
						|
	for _, v := range in.Files {
 | 
						|
		if v.MetaFile {
 | 
						|
			list = append(list, filepath.Join(baseDir, filepath.FromSlash(v.File)))
 | 
						|
		}
 | 
						|
	}
 | 
						|
	return list
 | 
						|
}
 | 
						|
 | 
						|
// GetFilePath attempts to get the path of the destination index file as it is stored on disk
 | 
						|
func (in Index) GetFilePath(f IndexFile) string {
 | 
						|
	return filepath.Join(filepath.Dir(in.indexFile), filepath.FromSlash(f.File))
 | 
						|
}
 | 
						|
 | 
						|
// SaveFile attempts to read the file from disk
 | 
						|
func (in Index) SaveFile(f IndexFile, dest io.Writer) error {
 | 
						|
	hashFormat := f.HashFormat
 | 
						|
	if hashFormat == "" {
 | 
						|
		hashFormat = in.HashFormat
 | 
						|
	}
 | 
						|
	src, err := os.Open(in.GetFilePath(f))
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
	h, err := GetHashImpl(hashFormat)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	w := io.MultiWriter(h, dest)
 | 
						|
	_, err = io.Copy(w, src)
 | 
						|
	if err != nil {
 | 
						|
		return err
 | 
						|
	}
 | 
						|
 | 
						|
	calculatedHash := hex.EncodeToString(h.Sum(nil))
 | 
						|
	if calculatedHash != f.Hash && !viper.GetBool("no-internal-hashes") {
 | 
						|
		return errors.New("hash of saved file is invalid")
 | 
						|
	}
 | 
						|
 | 
						|
	return nil
 | 
						|
}
 |