mirror of
https://github.com/packwiz/packwiz.git
synced 2025-04-19 21:16:30 +02:00
* Fix ignore file inconsistencies .packwizignore is supposed to work like .gitignore, yet it fails miserably at doing so, being able to ignore only global patterns like *.zip, and failing at patterns like /*.zip. This commit introduces two changes to fix the issue: - First, it uses a more up-to-date library. denormal/go-gitignore has not been updated since 2018 and has long open issues and pull requests, one of which tries to address leading slash ignoring, a-la /*.zip. denormal account seems to be abandoned since around the same year, so it makes sense to find a new library that does roughly the same thing. Gladly so, there's actually a library by sabhiram that shares the same name (but not the package name itself - which is just ignore - so it probably requires aliasing when importing). - Secondly, it checks relative paths against ignore file instead of the absolute ones, which makes it possible to use leading slash (and probably some other features) with the new library. From personal tests, it seems to address most - if not all - of the inconsistencies. However, since it's a different library more throughout testing probably wouldn't hurt to make sure it didn't break anything. * Move dependency to main block
368 lines
9.1 KiB
Go
368 lines
9.1 KiB
Go
package core
|
|
|
|
import (
|
|
"errors"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/BurntSushi/toml"
|
|
gitignore "github.com/sabhiram/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, stringer, 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 = stringer.HashToString(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.CompileIgnoreFile(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.MatchesPath(path) {
|
|
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, stringer, err := GetHashImpl(hashFormat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
w := io.MultiWriter(h, dest)
|
|
_, err = io.Copy(w, src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
calculatedHash := stringer.HashToString(h.Sum(nil))
|
|
if calculatedHash != f.Hash && !viper.GetBool("no-internal-hashes") {
|
|
return errors.New("hash of saved file is invalid")
|
|
}
|
|
|
|
return nil
|
|
}
|