forked from Plemya-x/ALR
		
	Initial commit
This commit is contained in:
		
							
								
								
									
										172
									
								
								internal/cliutils/prompt.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								internal/cliutils/prompt.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,172 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package cliutils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/AlecAivazis/survey/v2"
 | 
			
		||||
	"lure.sh/lure/internal/config"
 | 
			
		||||
	"lure.sh/lure/internal/db"
 | 
			
		||||
	"lure.sh/lure/internal/pager"
 | 
			
		||||
	"lure.sh/lure/internal/translations"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// YesNoPrompt asks the user a yes or no question, using def as the default answer
 | 
			
		||||
func YesNoPrompt(ctx context.Context, msg string, interactive, def bool) (bool, error) {
 | 
			
		||||
	if interactive {
 | 
			
		||||
		var answer bool
 | 
			
		||||
		err := survey.AskOne(
 | 
			
		||||
			&survey.Confirm{
 | 
			
		||||
				Message: translations.Translator(ctx).TranslateTo(msg, config.Language(ctx)),
 | 
			
		||||
				Default: def,
 | 
			
		||||
			},
 | 
			
		||||
			&answer,
 | 
			
		||||
		)
 | 
			
		||||
		return answer, err
 | 
			
		||||
	} else {
 | 
			
		||||
		return def, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PromptViewScript asks the user if they'd like to see a script,
 | 
			
		||||
// shows it if they answer yes, then asks if they'd still like to
 | 
			
		||||
// continue, and exits if they answer no.
 | 
			
		||||
func PromptViewScript(ctx context.Context, script, name, style string, interactive bool) error {
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
 | 
			
		||||
	if !interactive {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	scriptPrompt := translations.Translator(ctx).TranslateTo("Would you like to view the build script for", config.Language(ctx)) + " " + name
 | 
			
		||||
	view, err := YesNoPrompt(ctx, scriptPrompt, interactive, false)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if view {
 | 
			
		||||
		err = ShowScript(script, name, style)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cont, err := YesNoPrompt(ctx, "Would you still like to continue?", interactive, false)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !cont {
 | 
			
		||||
			log.Fatal(translations.Translator(ctx).TranslateTo("User chose not to continue after reading script", config.Language(ctx))).Send()
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ShowScript uses the built-in pager to display a script at a
 | 
			
		||||
// given path, in the given syntax highlighting style.
 | 
			
		||||
func ShowScript(path, name, style string) error {
 | 
			
		||||
	scriptFl, err := os.Open(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer scriptFl.Close()
 | 
			
		||||
 | 
			
		||||
	str, err := pager.SyntaxHighlightBash(scriptFl, style)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pgr := pager.New(name, str)
 | 
			
		||||
	return pgr.Run()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FlattenPkgs attempts to flatten the a map of slices of packages into a single slice
 | 
			
		||||
// of packages by prompting the user if multiple packages match.
 | 
			
		||||
func FlattenPkgs(ctx context.Context, found map[string][]db.Package, verb string, interactive bool) []db.Package {
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	var outPkgs []db.Package
 | 
			
		||||
	for _, pkgs := range found {
 | 
			
		||||
		if len(pkgs) > 1 && interactive {
 | 
			
		||||
			choice, err := PkgPrompt(ctx, pkgs, verb, interactive)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error prompting for choice of package").Send()
 | 
			
		||||
			}
 | 
			
		||||
			outPkgs = append(outPkgs, choice)
 | 
			
		||||
		} else if len(pkgs) == 1 || !interactive {
 | 
			
		||||
			outPkgs = append(outPkgs, pkgs[0])
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return outPkgs
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// PkgPrompt asks the user to choose between multiple packages.
 | 
			
		||||
func PkgPrompt(ctx context.Context, options []db.Package, verb string, interactive bool) (db.Package, error) {
 | 
			
		||||
	if !interactive {
 | 
			
		||||
		return options[0], nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	names := make([]string, len(options))
 | 
			
		||||
	for i, option := range options {
 | 
			
		||||
		names[i] = option.Repository + "/" + option.Name + " " + option.Version
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prompt := &survey.Select{
 | 
			
		||||
		Options: names,
 | 
			
		||||
		Message: translations.Translator(ctx).TranslateTo("Choose which package to "+verb, config.Language(ctx)),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var choice int
 | 
			
		||||
	err := survey.AskOne(prompt, &choice)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return db.Package{}, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return options[choice], nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ChooseOptDepends asks the user to choose between multiple optional dependencies.
 | 
			
		||||
// The user may choose multiple items.
 | 
			
		||||
func ChooseOptDepends(ctx context.Context, options []string, verb string, interactive bool) ([]string, error) {
 | 
			
		||||
	if !interactive {
 | 
			
		||||
		return []string{}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prompt := &survey.MultiSelect{
 | 
			
		||||
		Options: options,
 | 
			
		||||
		Message: translations.Translator(ctx).TranslateTo("Choose which optional package(s) to install", config.Language(ctx)),
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var choices []int
 | 
			
		||||
	err := survey.AskOne(prompt, &choices)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := make([]string, len(choices))
 | 
			
		||||
	for i, choiceIndex := range choices {
 | 
			
		||||
		out[i], _, _ = strings.Cut(options[choiceIndex], ": ")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										79
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										79
									
								
								internal/config/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,79 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"lure.sh/lure/internal/types"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var defaultConfig = &types.Config{
 | 
			
		||||
	RootCmd:          "sudo",
 | 
			
		||||
	PagerStyle:       "native",
 | 
			
		||||
	IgnorePkgUpdates: []string{},
 | 
			
		||||
	Repos: []types.Repo{
 | 
			
		||||
		{
 | 
			
		||||
			Name: "default",
 | 
			
		||||
			URL:  "https://github.com/lure-sh/lure-repo.git",
 | 
			
		||||
		},
 | 
			
		||||
	},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	configMtx sync.Mutex
 | 
			
		||||
	config    *types.Config
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Config returns a LURE configuration struct.
 | 
			
		||||
// The first time it's called, it'll load the config from a file.
 | 
			
		||||
// Subsequent calls will just return the same value.
 | 
			
		||||
func Config(ctx context.Context) *types.Config {
 | 
			
		||||
	configMtx.Lock()
 | 
			
		||||
	defer configMtx.Unlock()
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
 | 
			
		||||
	if config == nil {
 | 
			
		||||
		cfgFl, err := os.Open(GetPaths(ctx).ConfigPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Warn("Error opening config file, using defaults").Err(err).Send()
 | 
			
		||||
			return defaultConfig
 | 
			
		||||
		}
 | 
			
		||||
		defer cfgFl.Close()
 | 
			
		||||
 | 
			
		||||
		// Copy the default configuration into config
 | 
			
		||||
		defCopy := *defaultConfig
 | 
			
		||||
		config = &defCopy
 | 
			
		||||
		config.Repos = nil
 | 
			
		||||
 | 
			
		||||
		err = toml.NewDecoder(cfgFl).Decode(config)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Warn("Error decoding config file, using defaults").Err(err).Send()
 | 
			
		||||
			// Set config back to nil so that we try again next time
 | 
			
		||||
			config = nil
 | 
			
		||||
			return defaultConfig
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return config
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										67
									
								
								internal/config/lang.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								internal/config/lang.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,67 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	langMtx sync.Mutex
 | 
			
		||||
	lang    language.Tag
 | 
			
		||||
	langSet bool
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Language returns the system language.
 | 
			
		||||
// The first time it's called, it'll detect the langauge based on
 | 
			
		||||
// the $LANG environment variable.
 | 
			
		||||
// Subsequent calls will just return the same value.
 | 
			
		||||
func Language(ctx context.Context) language.Tag {
 | 
			
		||||
	langMtx.Lock()
 | 
			
		||||
	defer langMtx.Unlock()
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	if !langSet {
 | 
			
		||||
		syslang := SystemLang()
 | 
			
		||||
		tag, err := language.Parse(syslang)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error parsing system language").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
		base, _ := tag.Base()
 | 
			
		||||
		lang = language.Make(base.String())
 | 
			
		||||
		langSet = true
 | 
			
		||||
	}
 | 
			
		||||
	return lang
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// SystemLang returns the system language based on
 | 
			
		||||
// the $LANG environment variable.
 | 
			
		||||
func SystemLang() string {
 | 
			
		||||
	lang := os.Getenv("LANG")
 | 
			
		||||
	lang, _, _ = strings.Cut(lang, ".")
 | 
			
		||||
	if lang == "" || lang == "C" {
 | 
			
		||||
		lang = "en"
 | 
			
		||||
	}
 | 
			
		||||
	return lang
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										108
									
								
								internal/config/paths.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								internal/config/paths.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,108 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/pelletier/go-toml/v2"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Paths contains various paths used by LURE
 | 
			
		||||
type Paths struct {
 | 
			
		||||
	ConfigDir  string
 | 
			
		||||
	ConfigPath string
 | 
			
		||||
	CacheDir   string
 | 
			
		||||
	RepoDir    string
 | 
			
		||||
	PkgsDir    string
 | 
			
		||||
	DBPath     string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	pathsMtx sync.Mutex
 | 
			
		||||
	paths    *Paths
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GetPaths returns a Paths struct.
 | 
			
		||||
// The first time it's called, it'll generate the struct
 | 
			
		||||
// using information from the system.
 | 
			
		||||
// Subsequent calls will return the same value.
 | 
			
		||||
func GetPaths(ctx context.Context) *Paths {
 | 
			
		||||
	pathsMtx.Lock()
 | 
			
		||||
	defer pathsMtx.Unlock()
 | 
			
		||||
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	if paths == nil {
 | 
			
		||||
		paths = &Paths{}
 | 
			
		||||
 | 
			
		||||
		cfgDir, err := os.UserConfigDir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to detect user config directory").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		paths.ConfigDir = filepath.Join(cfgDir, "lure")
 | 
			
		||||
 | 
			
		||||
		err = os.MkdirAll(paths.ConfigDir, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to create LURE config directory").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		paths.ConfigPath = filepath.Join(paths.ConfigDir, "lure.toml")
 | 
			
		||||
 | 
			
		||||
		if _, err := os.Stat(paths.ConfigPath); err != nil {
 | 
			
		||||
			cfgFl, err := os.Create(paths.ConfigPath)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Unable to create LURE config file").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = toml.NewEncoder(cfgFl).Encode(&defaultConfig)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				log.Fatal("Error encoding default configuration").Err(err).Send()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			cfgFl.Close()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cacheDir, err := os.UserCacheDir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to detect cache directory").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		paths.CacheDir = filepath.Join(cacheDir, "lure")
 | 
			
		||||
		paths.RepoDir = filepath.Join(paths.CacheDir, "repo")
 | 
			
		||||
		paths.PkgsDir = filepath.Join(paths.CacheDir, "pkgs")
 | 
			
		||||
 | 
			
		||||
		err = os.MkdirAll(paths.RepoDir, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to create repo cache directory").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = os.MkdirAll(paths.PkgsDir, 0o755)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Unable to create package cache directory").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		paths.DBPath = filepath.Join(paths.CacheDir, "db")
 | 
			
		||||
	}
 | 
			
		||||
	return paths
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										5
									
								
								internal/config/version.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								internal/config/version.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
package config
 | 
			
		||||
 | 
			
		||||
// Version contains the version of LURE. If the version
 | 
			
		||||
// isn't known, it'll be set to "unknown"
 | 
			
		||||
var Version = "unknown"
 | 
			
		||||
							
								
								
									
										117
									
								
								internal/cpu/cpu.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										117
									
								
								internal/cpu/cpu.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,117 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package cpu
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"golang.org/x/sys/cpu"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// armVariant checks which variant of ARM lure is running
 | 
			
		||||
// on, by using the same detection method as Go itself
 | 
			
		||||
func armVariant() string {
 | 
			
		||||
	armEnv := os.Getenv("LURE_ARM_VARIANT")
 | 
			
		||||
	// ensure value has "arm" prefix, such as arm5 or arm6
 | 
			
		||||
	if strings.HasPrefix(armEnv, "arm") {
 | 
			
		||||
		return armEnv
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if cpu.ARM.HasVFPv3 {
 | 
			
		||||
		return "arm7"
 | 
			
		||||
	} else if cpu.ARM.HasVFP {
 | 
			
		||||
		return "arm6"
 | 
			
		||||
	} else {
 | 
			
		||||
		return "arm5"
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Arch returns the canonical CPU architecture of the system
 | 
			
		||||
func Arch() string {
 | 
			
		||||
	arch := os.Getenv("LURE_ARCH")
 | 
			
		||||
	if arch == "" {
 | 
			
		||||
		arch = runtime.GOARCH
 | 
			
		||||
	}
 | 
			
		||||
	if arch == "arm" {
 | 
			
		||||
		arch = armVariant()
 | 
			
		||||
	}
 | 
			
		||||
	return arch
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func IsCompatibleWith(target string, list []string) bool {
 | 
			
		||||
	if target == "all" || slices.Contains(list, "all") {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, arch := range list {
 | 
			
		||||
		if strings.HasPrefix(target, "arm") && strings.HasPrefix(arch, "arm") {
 | 
			
		||||
			targetVer, err := getARMVersion(target)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			archVer, err := getARMVersion(arch)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if targetVer >= archVer {
 | 
			
		||||
				return true
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if target == arch {
 | 
			
		||||
			return true
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return false
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func CompatibleArches(arch string) ([]string, error) {
 | 
			
		||||
	if strings.HasPrefix(arch, "arm") {
 | 
			
		||||
		ver, err := getARMVersion(arch)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if ver > 5 {
 | 
			
		||||
			var out []string
 | 
			
		||||
			for i := ver; i >= 5; i-- {
 | 
			
		||||
				out = append(out, "arm"+strconv.Itoa(i))
 | 
			
		||||
			}
 | 
			
		||||
			return out, nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return []string{arch}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getARMVersion(arch string) (int, error) {
 | 
			
		||||
	// Extract the version number from ARM architecture
 | 
			
		||||
	version := strings.TrimPrefix(arch, "arm")
 | 
			
		||||
	if version == "" {
 | 
			
		||||
		return 5, nil // Default to arm5 if version is not specified
 | 
			
		||||
	}
 | 
			
		||||
	return strconv.Atoi(version)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										346
									
								
								internal/db/db.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										346
									
								
								internal/db/db.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,346 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package db
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"database/sql"
 | 
			
		||||
	"database/sql/driver"
 | 
			
		||||
	"encoding/json"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"lure.sh/lure/internal/config"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"modernc.org/sqlite"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// CurrentVersion is the current version of the database.
 | 
			
		||||
// The database is reset if its version doesn't match this.
 | 
			
		||||
const CurrentVersion = 2
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	sqlite.MustRegisterScalarFunction("json_array_contains", 2, jsonArrayContains)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Package is a LURE package's database representation
 | 
			
		||||
type Package struct {
 | 
			
		||||
	Name          string                    `sh:"name,required" db:"name"`
 | 
			
		||||
	Version       string                    `sh:"version,required" db:"version"`
 | 
			
		||||
	Release       int                       `sh:"release,required" db:"release"`
 | 
			
		||||
	Epoch         uint                      `sh:"epoch" db:"epoch"`
 | 
			
		||||
	Description   JSON[map[string]string]   `db:"description"`
 | 
			
		||||
	Homepage      JSON[map[string]string]   `db:"homepage"`
 | 
			
		||||
	Maintainer    JSON[map[string]string]   `db:"maintainer"`
 | 
			
		||||
	Architectures JSON[[]string]            `sh:"architectures" db:"architectures"`
 | 
			
		||||
	Licenses      JSON[[]string]            `sh:"license" db:"licenses"`
 | 
			
		||||
	Provides      JSON[[]string]            `sh:"provides" db:"provides"`
 | 
			
		||||
	Conflicts     JSON[[]string]            `sh:"conflicts" db:"conflicts"`
 | 
			
		||||
	Replaces      JSON[[]string]            `sh:"replaces" db:"replaces"`
 | 
			
		||||
	Depends       JSON[map[string][]string] `db:"depends"`
 | 
			
		||||
	BuildDepends  JSON[map[string][]string] `db:"builddepends"`
 | 
			
		||||
	OptDepends    JSON[map[string][]string] `db:"optdepends"`
 | 
			
		||||
	Repository    string                    `db:"repository"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type version struct {
 | 
			
		||||
	Version int `db:"version"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	mu     sync.Mutex
 | 
			
		||||
	conn   *sqlx.DB
 | 
			
		||||
	closed = true
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// DB returns the LURE database.
 | 
			
		||||
// The first time it's called, it opens the SQLite database file.
 | 
			
		||||
// Subsequent calls return the same connection.
 | 
			
		||||
func DB(ctx context.Context) *sqlx.DB {
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	if conn != nil && !closed {
 | 
			
		||||
		return getConn()
 | 
			
		||||
	}
 | 
			
		||||
	_, err := open(ctx, config.GetPaths(ctx).DBPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		log.Fatal("Error opening database").Err(err).Send()
 | 
			
		||||
	}
 | 
			
		||||
	return getConn()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getConn() *sqlx.DB {
 | 
			
		||||
	mu.Lock()
 | 
			
		||||
	defer mu.Unlock()
 | 
			
		||||
	return conn
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func open(ctx context.Context, dsn string) (*sqlx.DB, error) {
 | 
			
		||||
	db, err := sqlx.Open("sqlite", dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	mu.Lock()
 | 
			
		||||
	conn = db
 | 
			
		||||
	closed = false
 | 
			
		||||
	mu.Unlock()
 | 
			
		||||
 | 
			
		||||
	err = initDB(ctx, dsn)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return db, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Close closes the database
 | 
			
		||||
func Close() error {
 | 
			
		||||
	closed = true
 | 
			
		||||
	if conn != nil {
 | 
			
		||||
		return conn.Close()
 | 
			
		||||
	} else {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// initDB initializes the database
 | 
			
		||||
func initDB(ctx context.Context, dsn string) error {
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	conn = conn.Unsafe()
 | 
			
		||||
	_, err := conn.ExecContext(ctx, `
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS pkgs (
 | 
			
		||||
			name          TEXT NOT NULL,
 | 
			
		||||
			repository    TEXT NOT NULL,
 | 
			
		||||
			version       TEXT NOT NULL,
 | 
			
		||||
			release       INT  NOT NULL,
 | 
			
		||||
			epoch         INT,
 | 
			
		||||
			description   TEXT CHECK(description = 'null' OR (JSON_VALID(description) AND JSON_TYPE(description) = 'object')),
 | 
			
		||||
			homepage      TEXT CHECK(homepage = 'null' OR (JSON_VALID(homepage) AND JSON_TYPE(homepage) = 'object')),
 | 
			
		||||
			maintainer    TEXT CHECK(maintainer = 'null' OR (JSON_VALID(maintainer) AND JSON_TYPE(maintainer) = 'object')),
 | 
			
		||||
			architectures TEXT CHECK(architectures = 'null' OR (JSON_VALID(architectures) AND JSON_TYPE(architectures) = 'array')),
 | 
			
		||||
			licenses      TEXT CHECK(licenses = 'null' OR (JSON_VALID(licenses) AND JSON_TYPE(licenses) = 'array')),
 | 
			
		||||
			provides      TEXT CHECK(provides = 'null' OR (JSON_VALID(provides) AND JSON_TYPE(provides) = 'array')),
 | 
			
		||||
			conflicts     TEXT CHECK(conflicts = 'null' OR (JSON_VALID(conflicts) AND JSON_TYPE(conflicts) = 'array')),
 | 
			
		||||
			replaces      TEXT CHECK(replaces = 'null' OR (JSON_VALID(replaces) AND JSON_TYPE(replaces) = 'array')),
 | 
			
		||||
			depends       TEXT CHECK(depends = 'null' OR (JSON_VALID(depends) AND JSON_TYPE(depends) = 'object')),
 | 
			
		||||
			builddepends  TEXT CHECK(builddepends = 'null' OR (JSON_VALID(builddepends) AND JSON_TYPE(builddepends) = 'object')),
 | 
			
		||||
			optdepends    TEXT CHECK(optdepends = 'null' OR (JSON_VALID(optdepends) AND JSON_TYPE(optdepends) = 'object')),
 | 
			
		||||
			UNIQUE(name, repository)
 | 
			
		||||
		);
 | 
			
		||||
 | 
			
		||||
		CREATE TABLE IF NOT EXISTS lure_db_version (
 | 
			
		||||
			version INT NOT NULL
 | 
			
		||||
		);
 | 
			
		||||
	`)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ver, ok := GetVersion(ctx)
 | 
			
		||||
	if ok && ver != CurrentVersion {
 | 
			
		||||
		log.Warn("Database version mismatch; resetting").Int("version", ver).Int("expected", CurrentVersion).Send()
 | 
			
		||||
		reset(ctx)
 | 
			
		||||
		return initDB(ctx, dsn)
 | 
			
		||||
	} else if !ok {
 | 
			
		||||
		log.Warn("Database version does not exist. Run lure fix if something isn't working.").Send()
 | 
			
		||||
		return addVersion(ctx, CurrentVersion)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// reset drops all the database tables
 | 
			
		||||
func reset(ctx context.Context) error {
 | 
			
		||||
	_, err := DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS pkgs;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	_, err = DB(ctx).ExecContext(ctx, "DROP TABLE IF EXISTS lure_db_version;")
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IsEmpty returns true if the database has no packages in it, otherwise it returns false.
 | 
			
		||||
func IsEmpty(ctx context.Context) bool {
 | 
			
		||||
	var count int
 | 
			
		||||
	err := DB(ctx).GetContext(ctx, &count, "SELECT count(1) FROM pkgs;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return true
 | 
			
		||||
	}
 | 
			
		||||
	return count == 0
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetVersion returns the database version and a boolean indicating
 | 
			
		||||
// whether the database contained a version number
 | 
			
		||||
func GetVersion(ctx context.Context) (int, bool) {
 | 
			
		||||
	var ver version
 | 
			
		||||
	err := DB(ctx).GetContext(ctx, &ver, "SELECT * FROM lure_db_version LIMIT 1;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, false
 | 
			
		||||
	}
 | 
			
		||||
	return ver.Version, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func addVersion(ctx context.Context, ver int) error {
 | 
			
		||||
	_, err := DB(ctx).ExecContext(ctx, `INSERT INTO lure_db_version(version) VALUES (?);`, ver)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// InsertPackage adds a package to the database
 | 
			
		||||
func InsertPackage(ctx context.Context, pkg Package) error {
 | 
			
		||||
	_, err := DB(ctx).NamedExecContext(ctx, `
 | 
			
		||||
		INSERT OR REPLACE INTO pkgs (
 | 
			
		||||
			name,
 | 
			
		||||
			repository,
 | 
			
		||||
			version,
 | 
			
		||||
			release,
 | 
			
		||||
			epoch,
 | 
			
		||||
			description,
 | 
			
		||||
			homepage,
 | 
			
		||||
			maintainer,
 | 
			
		||||
			architectures,
 | 
			
		||||
			licenses,
 | 
			
		||||
			provides,
 | 
			
		||||
			conflicts,
 | 
			
		||||
			replaces,
 | 
			
		||||
			depends,
 | 
			
		||||
			builddepends,
 | 
			
		||||
			optdepends
 | 
			
		||||
		) VALUES (
 | 
			
		||||
			:name,
 | 
			
		||||
			:repository,
 | 
			
		||||
			:version,
 | 
			
		||||
			:release,
 | 
			
		||||
			:epoch,
 | 
			
		||||
			:description,
 | 
			
		||||
			:homepage,
 | 
			
		||||
			:maintainer,
 | 
			
		||||
			:architectures,
 | 
			
		||||
			:licenses,
 | 
			
		||||
			:provides,
 | 
			
		||||
			:conflicts,
 | 
			
		||||
			:replaces,
 | 
			
		||||
			:depends,
 | 
			
		||||
			:builddepends,
 | 
			
		||||
			:optdepends
 | 
			
		||||
		);
 | 
			
		||||
	`, pkg)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPkgs returns a result containing packages that match the where conditions
 | 
			
		||||
func GetPkgs(ctx context.Context, where string, args ...any) (*sqlx.Rows, error) {
 | 
			
		||||
	stream, err := DB(ctx).QueryxContext(ctx, "SELECT * FROM pkgs WHERE "+where, args...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return stream, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetPkg returns a single package that matches the where conditions
 | 
			
		||||
func GetPkg(ctx context.Context, where string, args ...any) (*Package, error) {
 | 
			
		||||
	out := &Package{}
 | 
			
		||||
	err := DB(ctx).GetContext(ctx, out, "SELECT * FROM pkgs WHERE "+where+" LIMIT 1", args...)
 | 
			
		||||
	return out, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DeletePkgs deletes all packages matching the where conditions
 | 
			
		||||
func DeletePkgs(ctx context.Context, where string, args ...any) error {
 | 
			
		||||
	_, err := DB(ctx).ExecContext(ctx, "DELETE FROM pkgs WHERE "+where, args...)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// jsonArrayContains is an SQLite function that checks if a JSON array
 | 
			
		||||
// in the database contains a given value
 | 
			
		||||
func jsonArrayContains(ctx *sqlite.FunctionContext, args []driver.Value) (driver.Value, error) {
 | 
			
		||||
	value, ok := args[0].(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, errors.New("both arguments to json_array_contains must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	item, ok := args[1].(string)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		return nil, errors.New("both arguments to json_array_contains must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var array []string
 | 
			
		||||
	err := json.Unmarshal([]byte(value), &array)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return slices.Contains(array, item), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// JSON represents a JSON value in the database
 | 
			
		||||
type JSON[T any] struct {
 | 
			
		||||
	Val T
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// NewJSON creates a new database JSON value
 | 
			
		||||
func NewJSON[T any](v T) JSON[T] {
 | 
			
		||||
	return JSON[T]{Val: v}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s *JSON[T]) Scan(val any) error {
 | 
			
		||||
	if val == nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch val := val.(type) {
 | 
			
		||||
	case string:
 | 
			
		||||
		err := json.Unmarshal([]byte(val), &s.Val)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	case sql.NullString:
 | 
			
		||||
		if val.Valid {
 | 
			
		||||
			err := json.Unmarshal([]byte(val.String), &s.Val)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	default:
 | 
			
		||||
		return errors.New("sqlite json types must be strings")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) Value() (driver.Value, error) {
 | 
			
		||||
	data, err := json.Marshal(s.Val)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
	return string(data), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) MarshalYAML() (any, error) {
 | 
			
		||||
	return s.Val, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) String() string {
 | 
			
		||||
	return fmt.Sprint(s.Val)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (s JSON[T]) GoString() string {
 | 
			
		||||
	return fmt.Sprintf("%#v", s.Val)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								internal/db/db_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										250
									
								
								internal/db/db_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,250 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package db_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/jmoiron/sqlx"
 | 
			
		||||
	"lure.sh/lure/internal/db"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var testPkg = db.Package{
 | 
			
		||||
	Name:    "test",
 | 
			
		||||
	Version: "0.0.1",
 | 
			
		||||
	Release: 1,
 | 
			
		||||
	Epoch:   2,
 | 
			
		||||
	Description: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "Test package",
 | 
			
		||||
		"ru": "Проверочный пакет",
 | 
			
		||||
	}),
 | 
			
		||||
	Homepage: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "https://lure.sh/",
 | 
			
		||||
	}),
 | 
			
		||||
	Maintainer: db.NewJSON(map[string]string{
 | 
			
		||||
		"en": "Elara Musayelyan <elara@elara.ws>",
 | 
			
		||||
		"ru": "Элара Мусаелян <elara@elara.ws>",
 | 
			
		||||
	}),
 | 
			
		||||
	Architectures: db.NewJSON([]string{"arm64", "amd64"}),
 | 
			
		||||
	Licenses:      db.NewJSON([]string{"GPL-3.0-or-later"}),
 | 
			
		||||
	Provides:      db.NewJSON([]string{"test"}),
 | 
			
		||||
	Conflicts:     db.NewJSON([]string{"test"}),
 | 
			
		||||
	Replaces:      db.NewJSON([]string{"test-old"}),
 | 
			
		||||
	Depends: db.NewJSON(map[string][]string{
 | 
			
		||||
		"": {"sudo"},
 | 
			
		||||
	}),
 | 
			
		||||
	BuildDepends: db.NewJSON(map[string][]string{
 | 
			
		||||
		"":     {"golang"},
 | 
			
		||||
		"arch": {"go"},
 | 
			
		||||
	}),
 | 
			
		||||
	Repository: "default",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInit(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = db.DB().Exec("SELECT * FROM pkgs")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	ver, ok := db.GetVersion()
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Errorf("Expected version to be present")
 | 
			
		||||
	} else if ver != db.CurrentVersion {
 | 
			
		||||
		t.Errorf("Expected version %d, got %d", db.CurrentVersion, ver)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestInsertPackage(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(testPkg)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dbPkg := db.Package{}
 | 
			
		||||
	err = sqlx.Get(db.DB(), &dbPkg, "SELECT * FROM pkgs WHERE name = 'test' AND repository = 'default'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(testPkg, dbPkg) {
 | 
			
		||||
		t.Errorf("Expected test package to be the same as database package")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetPkgs(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result, err := db.GetPkgs("name LIKE 'x%'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for result.Next() {
 | 
			
		||||
		var dbPkg db.Package
 | 
			
		||||
		err = result.StructScan(&dbPkg)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !strings.HasPrefix(dbPkg.Name, "x") {
 | 
			
		||||
			t.Errorf("Expected package name to start with 'x', got %s", dbPkg.Name)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetPkg(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pkg, err := db.GetPkg("name LIKE 'x%' ORDER BY name")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if pkg.Name != "x1" {
 | 
			
		||||
		t.Errorf("Expected x1 package, got %s", pkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(*pkg, x1) {
 | 
			
		||||
		t.Errorf("Expected x1 to be %v, got %v", x1, *pkg)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDeletePkgs(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.DeletePkgs("name = 'x1'")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dbPkg db.Package
 | 
			
		||||
	err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE name LIKE 'x%' ORDER BY name LIMIT 1;")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dbPkg.Name != "x2" {
 | 
			
		||||
		t.Errorf("Expected x2 package, got %s", dbPkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestJsonArrayContains(t *testing.T) {
 | 
			
		||||
	_, err := db.Open(":memory:")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
	defer db.Close()
 | 
			
		||||
 | 
			
		||||
	x1 := testPkg
 | 
			
		||||
	x1.Name = "x1"
 | 
			
		||||
	x2 := testPkg
 | 
			
		||||
	x2.Name = "x2"
 | 
			
		||||
	x2.Provides.Val = append(x2.Provides.Val, "x")
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x1)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = db.InsertPackage(x2)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var dbPkg db.Package
 | 
			
		||||
	err = db.DB().Get(&dbPkg, "SELECT * FROM pkgs WHERE json_array_contains(provides, 'x');")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if dbPkg.Name != "x2" {
 | 
			
		||||
		t.Errorf("Expected x2 package, got %s", dbPkg.Name)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										379
									
								
								internal/dl/dl.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										379
									
								
								internal/dl/dl.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,379 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
// Package dl contains abstractions for downloadingfiles and directories
 | 
			
		||||
// from various sources.
 | 
			
		||||
package dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/md5"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"crypto/sha256"
 | 
			
		||||
	"crypto/sha512"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"hash"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/PuerkitoBio/purell"
 | 
			
		||||
	"github.com/vmihailenco/msgpack/v5"
 | 
			
		||||
	"golang.org/x/crypto/blake2b"
 | 
			
		||||
	"golang.org/x/crypto/blake2s"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"lure.sh/lure/internal/dlcache"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const manifestFileName = ".lure_cache_manifest"
 | 
			
		||||
 | 
			
		||||
// ErrChecksumMismatch occurs when the checksum of a downloaded file
 | 
			
		||||
// does not match the expected checksum provided in the Options struct.
 | 
			
		||||
var (
 | 
			
		||||
	ErrChecksumMismatch = errors.New("dl: checksums did not match")
 | 
			
		||||
	ErrNoSuchHashAlgo   = errors.New("dl: invalid hashing algorithm")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Downloaders contains all the downloaders in the order in which
 | 
			
		||||
// they should be checked
 | 
			
		||||
var Downloaders = []Downloader{
 | 
			
		||||
	GitDownloader{},
 | 
			
		||||
	TorrentDownloader{},
 | 
			
		||||
	FileDownloader{},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Type represents the type of download (file or directory)
 | 
			
		||||
type Type uint8
 | 
			
		||||
 | 
			
		||||
const (
 | 
			
		||||
	TypeFile Type = iota
 | 
			
		||||
	TypeDir
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func (t Type) String() string {
 | 
			
		||||
	switch t {
 | 
			
		||||
	case TypeFile:
 | 
			
		||||
		return "file"
 | 
			
		||||
	case TypeDir:
 | 
			
		||||
		return "dir"
 | 
			
		||||
	}
 | 
			
		||||
	return "<unknown>"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Options contains the options for downloading
 | 
			
		||||
// files and directories
 | 
			
		||||
type Options struct {
 | 
			
		||||
	Hash             []byte
 | 
			
		||||
	HashAlgorithm    string
 | 
			
		||||
	Name             string
 | 
			
		||||
	URL              string
 | 
			
		||||
	Destination      string
 | 
			
		||||
	CacheDisabled    bool
 | 
			
		||||
	PostprocDisabled bool
 | 
			
		||||
	Progress         io.Writer
 | 
			
		||||
	LocalDir         string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (opts Options) NewHash() (hash.Hash, error) {
 | 
			
		||||
	switch opts.HashAlgorithm {
 | 
			
		||||
	case "", "sha256":
 | 
			
		||||
		return sha256.New(), nil
 | 
			
		||||
	case "sha224":
 | 
			
		||||
		return sha256.New224(), nil
 | 
			
		||||
	case "sha512":
 | 
			
		||||
		return sha512.New(), nil
 | 
			
		||||
	case "sha384":
 | 
			
		||||
		return sha512.New384(), nil
 | 
			
		||||
	case "sha1":
 | 
			
		||||
		return sha1.New(), nil
 | 
			
		||||
	case "md5":
 | 
			
		||||
		return md5.New(), nil
 | 
			
		||||
	case "blake2s-128":
 | 
			
		||||
		return blake2s.New256(nil)
 | 
			
		||||
	case "blake2s-256":
 | 
			
		||||
		return blake2s.New256(nil)
 | 
			
		||||
	case "blake2b-256":
 | 
			
		||||
		return blake2b.New(32, nil)
 | 
			
		||||
	case "blake2b-512":
 | 
			
		||||
		return blake2b.New(64, nil)
 | 
			
		||||
	default:
 | 
			
		||||
		return nil, fmt.Errorf("%w: %s", ErrNoSuchHashAlgo, opts.HashAlgorithm)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Manifest holds information about the type and name
 | 
			
		||||
// of a downloaded file or directory. It is stored inside
 | 
			
		||||
// each cache directory for later use.
 | 
			
		||||
type Manifest struct {
 | 
			
		||||
	Type Type
 | 
			
		||||
	Name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Downloader interface {
 | 
			
		||||
	// Name returns the name of the downloader
 | 
			
		||||
	Name() string
 | 
			
		||||
	// MatchURL checks if the given URL matches
 | 
			
		||||
	// the downloader.
 | 
			
		||||
	MatchURL(string) bool
 | 
			
		||||
	// Download downloads the object at the URL
 | 
			
		||||
	// provided in the options, to the destination
 | 
			
		||||
	// given in the options. It returns a type,
 | 
			
		||||
	// a name for the downloaded object (this may be empty),
 | 
			
		||||
	// and an error.
 | 
			
		||||
	Download(Options) (Type, string, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// UpdatingDownloader extends the Downloader interface
 | 
			
		||||
// with an Update method for protocols such as git, which
 | 
			
		||||
// allow for incremental updates without changing the URL.
 | 
			
		||||
type UpdatingDownloader interface {
 | 
			
		||||
	Downloader
 | 
			
		||||
	// Update checks for and performs any
 | 
			
		||||
	// available updates for the object
 | 
			
		||||
	// described in the options. It returns
 | 
			
		||||
	// true if an update was performed, or
 | 
			
		||||
	// false if no update was required.
 | 
			
		||||
	Update(Options) (bool, error)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download downloads a file or directory using the specified options.
 | 
			
		||||
// It first gets the appropriate downloader for the URL, then checks
 | 
			
		||||
// if caching is enabled. If caching is enabled, it attempts to get
 | 
			
		||||
// the cache directory for the URL and update it if necessary.
 | 
			
		||||
// If the source is found in the cache, it links it to the destination
 | 
			
		||||
// using hard links. If the source is not found in the cache,
 | 
			
		||||
// it downloads the source to a new cache directory and links it
 | 
			
		||||
// to the destination.
 | 
			
		||||
func Download(ctx context.Context, opts Options) (err error) {
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	normalized, err := normalizeURL(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	opts.URL = normalized
 | 
			
		||||
 | 
			
		||||
	d := getDownloader(opts.URL)
 | 
			
		||||
 | 
			
		||||
	if opts.CacheDisabled {
 | 
			
		||||
		_, _, err = d.Download(opts)
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var t Type
 | 
			
		||||
	cacheDir, ok := dlcache.Get(ctx, opts.URL)
 | 
			
		||||
	if ok {
 | 
			
		||||
		var updated bool
 | 
			
		||||
		if d, ok := d.(UpdatingDownloader); ok {
 | 
			
		||||
			log.Info("Source can be updated, updating if required").Str("source", opts.Name).Str("downloader", d.Name()).Send()
 | 
			
		||||
 | 
			
		||||
			updated, err = d.Update(Options{
 | 
			
		||||
				Hash:          opts.Hash,
 | 
			
		||||
				HashAlgorithm: opts.HashAlgorithm,
 | 
			
		||||
				Name:          opts.Name,
 | 
			
		||||
				URL:           opts.URL,
 | 
			
		||||
				Destination:   cacheDir,
 | 
			
		||||
				Progress:      opts.Progress,
 | 
			
		||||
				LocalDir:      opts.LocalDir,
 | 
			
		||||
			})
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		m, err := getManifest(cacheDir)
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			t = m.Type
 | 
			
		||||
 | 
			
		||||
			dest := filepath.Join(opts.Destination, m.Name)
 | 
			
		||||
			ok, err := handleCache(cacheDir, dest, m.Name, t)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if ok && !updated {
 | 
			
		||||
				log.Info("Source found in cache and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
 | 
			
		||||
				return nil
 | 
			
		||||
			} else if ok {
 | 
			
		||||
				log.Info("Source updated and linked to destination").Str("source", opts.Name).Stringer("type", t).Send()
 | 
			
		||||
				return nil
 | 
			
		||||
			}
 | 
			
		||||
		} else {
 | 
			
		||||
			// If we cannot read the manifest,
 | 
			
		||||
			// this cache entry is invalid and
 | 
			
		||||
			// the source must be re-downloaded.
 | 
			
		||||
			err = os.RemoveAll(cacheDir)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	log.Info("Downloading source").Str("source", opts.Name).Str("downloader", d.Name()).Send()
 | 
			
		||||
 | 
			
		||||
	cacheDir, err = dlcache.New(ctx, opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t, name, err := d.Download(Options{
 | 
			
		||||
		Hash:          opts.Hash,
 | 
			
		||||
		HashAlgorithm: opts.HashAlgorithm,
 | 
			
		||||
		Name:          opts.Name,
 | 
			
		||||
		URL:           opts.URL,
 | 
			
		||||
		Destination:   cacheDir,
 | 
			
		||||
		Progress:      opts.Progress,
 | 
			
		||||
		LocalDir:      opts.LocalDir,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = writeManifest(cacheDir, Manifest{t, name})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dest := filepath.Join(opts.Destination, name)
 | 
			
		||||
	_, err = handleCache(cacheDir, dest, name, t)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// writeManifest writes the manifest to the specified cache directory.
 | 
			
		||||
func writeManifest(cacheDir string, m Manifest) error {
 | 
			
		||||
	fl, err := os.Create(filepath.Join(cacheDir, manifestFileName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
	return msgpack.NewEncoder(fl).Encode(m)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getManifest reads the manifest from the specified cache directory.
 | 
			
		||||
func getManifest(cacheDir string) (m Manifest, err error) {
 | 
			
		||||
	fl, err := os.Open(filepath.Join(cacheDir, manifestFileName))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return Manifest{}, err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
 | 
			
		||||
	err = msgpack.NewDecoder(fl).Decode(&m)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// handleCache links the cache directory or a file within it to the destination
 | 
			
		||||
func handleCache(cacheDir, dest, name string, t Type) (bool, error) {
 | 
			
		||||
	switch t {
 | 
			
		||||
	case TypeFile:
 | 
			
		||||
		cd, err := os.Open(cacheDir)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		names, err := cd.Readdirnames(0)
 | 
			
		||||
		if err == io.EOF {
 | 
			
		||||
			break
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cd.Close()
 | 
			
		||||
 | 
			
		||||
		if slices.Contains(names, name) {
 | 
			
		||||
			err = os.Link(filepath.Join(cacheDir, name), dest)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return false, err
 | 
			
		||||
			}
 | 
			
		||||
			return true, nil
 | 
			
		||||
		}
 | 
			
		||||
	case TypeDir:
 | 
			
		||||
		err := linkDir(cacheDir, dest)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
		return true, nil
 | 
			
		||||
	}
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// linkDir recursively walks through a directory, creating
 | 
			
		||||
// hard links for each file from the src directory to the
 | 
			
		||||
// dest directory. If it encounters a directory, it will
 | 
			
		||||
// create a directory with the same name and permissions
 | 
			
		||||
// in the dest directory, because hard links cannot be
 | 
			
		||||
// created for directories.
 | 
			
		||||
func linkDir(src, dest string) error {
 | 
			
		||||
	return filepath.Walk(src, func(path string, info os.FileInfo, err error) error {
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if info.Name() == manifestFileName {
 | 
			
		||||
			return nil
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		rel, err := filepath.Rel(src, path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newPath := filepath.Join(dest, rel)
 | 
			
		||||
		if info.IsDir() {
 | 
			
		||||
			return os.MkdirAll(newPath, info.Mode())
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return os.Link(path, newPath)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getDownloader(u string) Downloader {
 | 
			
		||||
	for _, d := range Downloaders {
 | 
			
		||||
		if d.MatchURL(u) {
 | 
			
		||||
			return d
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// normalizeURL normalizes a URL string, so that insignificant
 | 
			
		||||
// differences don't change the hash.
 | 
			
		||||
func normalizeURL(u string) (string, error) {
 | 
			
		||||
	const normalizationFlags = purell.FlagRemoveTrailingSlash |
 | 
			
		||||
		purell.FlagRemoveDefaultPort |
 | 
			
		||||
		purell.FlagLowercaseHost |
 | 
			
		||||
		purell.FlagLowercaseScheme |
 | 
			
		||||
		purell.FlagRemoveDuplicateSlashes |
 | 
			
		||||
		purell.FlagRemoveFragment |
 | 
			
		||||
		purell.FlagRemoveUnnecessaryHostDots |
 | 
			
		||||
		purell.FlagSortQuery |
 | 
			
		||||
		purell.FlagDecodeHexHost |
 | 
			
		||||
		purell.FlagDecodeOctalHost |
 | 
			
		||||
		purell.FlagDecodeUnnecessaryEscapes |
 | 
			
		||||
		purell.FlagRemoveEmptyPortSeparator
 | 
			
		||||
 | 
			
		||||
	u, err := purell.NormalizeURLString(u, normalizationFlags)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Fix magnet URLs after normalization
 | 
			
		||||
	u = strings.Replace(u, "magnet://", "magnet:", 1)
 | 
			
		||||
	return u, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										264
									
								
								internal/dl/file.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										264
									
								
								internal/dl/file.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,264 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"mime"
 | 
			
		||||
	"net/http"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"github.com/mholt/archiver/v4"
 | 
			
		||||
	"github.com/schollz/progressbar/v3"
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FileDownloader downloads files using HTTP
 | 
			
		||||
type FileDownloader struct{}
 | 
			
		||||
 | 
			
		||||
// Name always returns "file"
 | 
			
		||||
func (FileDownloader) Name() string {
 | 
			
		||||
	return "file"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchURL always returns true, as FileDownloader
 | 
			
		||||
// is used as a fallback if nothing else matches
 | 
			
		||||
func (FileDownloader) MatchURL(string) bool {
 | 
			
		||||
	return true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download downloads a file using HTTP. If the file is
 | 
			
		||||
// compressed using a supported format, it will be extracted
 | 
			
		||||
func (FileDownloader) Download(opts Options) (Type, string, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
 | 
			
		||||
	name := query.Get("~name")
 | 
			
		||||
	query.Del("~name")
 | 
			
		||||
 | 
			
		||||
	archive := query.Get("~archive")
 | 
			
		||||
	query.Del("~archive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	var r io.ReadCloser
 | 
			
		||||
	var size int64
 | 
			
		||||
	if u.Scheme == "local" {
 | 
			
		||||
		localFl, err := os.Open(filepath.Join(opts.LocalDir, u.Path))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
		fi, err := localFl.Stat()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
		size = fi.Size()
 | 
			
		||||
		if name == "" {
 | 
			
		||||
			name = fi.Name()
 | 
			
		||||
		}
 | 
			
		||||
		r = localFl
 | 
			
		||||
	} else {
 | 
			
		||||
		res, err := http.Get(u.String())
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
		size = res.ContentLength
 | 
			
		||||
		if name == "" {
 | 
			
		||||
			name = getFilename(res)
 | 
			
		||||
		}
 | 
			
		||||
		r = res.Body
 | 
			
		||||
	}
 | 
			
		||||
	defer r.Close()
 | 
			
		||||
 | 
			
		||||
	opts.PostprocDisabled = archive == "false"
 | 
			
		||||
 | 
			
		||||
	path := filepath.Join(opts.Destination, name)
 | 
			
		||||
	fl, err := os.Create(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	defer fl.Close()
 | 
			
		||||
 | 
			
		||||
	var bar io.WriteCloser
 | 
			
		||||
	if opts.Progress != nil {
 | 
			
		||||
		bar = progressbar.NewOptions64(
 | 
			
		||||
			size,
 | 
			
		||||
			progressbar.OptionSetDescription(name),
 | 
			
		||||
			progressbar.OptionSetWriter(opts.Progress),
 | 
			
		||||
			progressbar.OptionShowBytes(true),
 | 
			
		||||
			progressbar.OptionSetWidth(10),
 | 
			
		||||
			progressbar.OptionThrottle(65*time.Millisecond),
 | 
			
		||||
			progressbar.OptionShowCount(),
 | 
			
		||||
			progressbar.OptionOnCompletion(func() {
 | 
			
		||||
				_, _ = io.WriteString(opts.Progress, "\n")
 | 
			
		||||
			}),
 | 
			
		||||
			progressbar.OptionSpinnerType(14),
 | 
			
		||||
			progressbar.OptionFullWidth(),
 | 
			
		||||
			progressbar.OptionSetRenderBlankState(true),
 | 
			
		||||
		)
 | 
			
		||||
		defer bar.Close()
 | 
			
		||||
	} else {
 | 
			
		||||
		bar = handlers.NopRWC{}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	h, err := opts.NewHash()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var w io.Writer
 | 
			
		||||
	if opts.Hash != nil {
 | 
			
		||||
		w = io.MultiWriter(fl, h, bar)
 | 
			
		||||
	} else {
 | 
			
		||||
		w = io.MultiWriter(fl, bar)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(w, r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	r.Close()
 | 
			
		||||
 | 
			
		||||
	if opts.Hash != nil {
 | 
			
		||||
		sum := h.Sum(nil)
 | 
			
		||||
		if !bytes.Equal(sum, opts.Hash) {
 | 
			
		||||
			return 0, "", ErrChecksumMismatch
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if opts.PostprocDisabled {
 | 
			
		||||
		return TypeFile, name, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	_, err = fl.Seek(0, io.SeekStart)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	format, ar, err := archiver.Identify(name, fl)
 | 
			
		||||
	if err == archiver.ErrNoMatch {
 | 
			
		||||
		return TypeFile, name, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = extractFile(ar, format, name, opts)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.Remove(path)
 | 
			
		||||
	return TypeDir, "", err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// extractFile extracts an archive or decompresses a file
 | 
			
		||||
func extractFile(r io.Reader, format archiver.Format, name string, opts Options) (err error) {
 | 
			
		||||
	fname := format.Name()
 | 
			
		||||
 | 
			
		||||
	switch format := format.(type) {
 | 
			
		||||
	case archiver.Extractor:
 | 
			
		||||
		err = format.Extract(context.Background(), r, nil, func(ctx context.Context, f archiver.File) error {
 | 
			
		||||
			fr, err := f.Open()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			defer fr.Close()
 | 
			
		||||
			fi, err := f.Stat()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			fm := fi.Mode()
 | 
			
		||||
 | 
			
		||||
			path := filepath.Join(opts.Destination, f.NameInArchive)
 | 
			
		||||
 | 
			
		||||
			err = os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if f.IsDir() {
 | 
			
		||||
				err = os.Mkdir(path, 0o755)
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				outFl, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fm.Perm())
 | 
			
		||||
				if err != nil {
 | 
			
		||||
					return err
 | 
			
		||||
				}
 | 
			
		||||
				defer outFl.Close()
 | 
			
		||||
 | 
			
		||||
				_, err = io.Copy(outFl, fr)
 | 
			
		||||
				return err
 | 
			
		||||
			}
 | 
			
		||||
			return nil
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	case archiver.Decompressor:
 | 
			
		||||
		rc, err := format.OpenReader(r)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
		defer rc.Close()
 | 
			
		||||
 | 
			
		||||
		path := filepath.Join(opts.Destination, name)
 | 
			
		||||
		path = strings.TrimSuffix(path, fname)
 | 
			
		||||
 | 
			
		||||
		outFl, err := os.Create(path)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		_, err = io.Copy(outFl, rc)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getFilename attempts to parse the Content-Disposition
 | 
			
		||||
// HTTP response header and extract a filename. If the
 | 
			
		||||
// header does not exist, it will use the last element
 | 
			
		||||
// of the path.
 | 
			
		||||
func getFilename(res *http.Response) (name string) {
 | 
			
		||||
	_, params, err := mime.ParseMediaType(res.Header.Get("Content-Disposition"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return path.Base(res.Request.URL.Path)
 | 
			
		||||
	}
 | 
			
		||||
	if filename, ok := params["filename"]; ok {
 | 
			
		||||
		return filename
 | 
			
		||||
	} else {
 | 
			
		||||
		return path.Base(res.Request.URL.Path)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										198
									
								
								internal/dl/git.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								internal/dl/git.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,198 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"net/url"
 | 
			
		||||
	"path"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/config"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// GitDownloader downloads Git repositories
 | 
			
		||||
type GitDownloader struct{}
 | 
			
		||||
 | 
			
		||||
// Name always returns "git"
 | 
			
		||||
func (GitDownloader) Name() string {
 | 
			
		||||
	return "git"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchURL matches any URLs that start with "git+"
 | 
			
		||||
func (GitDownloader) MatchURL(u string) bool {
 | 
			
		||||
	return strings.HasPrefix(u, "git+")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download uses git to clone the repository from the specified URL.
 | 
			
		||||
// It allows specifying the revision, depth and recursion options
 | 
			
		||||
// via query string
 | 
			
		||||
func (GitDownloader) Download(opts Options) (Type, string, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
	u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
 | 
			
		||||
	rev := query.Get("~rev")
 | 
			
		||||
	query.Del("~rev")
 | 
			
		||||
 | 
			
		||||
	name := query.Get("~name")
 | 
			
		||||
	query.Del("~name")
 | 
			
		||||
 | 
			
		||||
	depthStr := query.Get("~depth")
 | 
			
		||||
	query.Del("~depth")
 | 
			
		||||
 | 
			
		||||
	recursive := query.Get("~recursive")
 | 
			
		||||
	query.Del("~recursive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	depth := 0
 | 
			
		||||
	if depthStr != "" {
 | 
			
		||||
		depth, err = strconv.Atoi(depthStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	co := &git.CloneOptions{
 | 
			
		||||
		URL:               u.String(),
 | 
			
		||||
		Depth:             depth,
 | 
			
		||||
		Progress:          opts.Progress,
 | 
			
		||||
		RecurseSubmodules: git.NoRecurseSubmodules,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recursive == "true" {
 | 
			
		||||
		co.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainClone(opts.Destination, false, co)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = r.Fetch(&git.FetchOptions{
 | 
			
		||||
		RefSpecs: []config.RefSpec{"+refs/*:refs/*"},
 | 
			
		||||
	})
 | 
			
		||||
	if err != git.NoErrAlreadyUpToDate && err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if rev != "" {
 | 
			
		||||
		h, err := r.ResolveRevision(plumbing.Revision(rev))
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		w, err := r.Worktree()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = w.Checkout(&git.CheckoutOptions{
 | 
			
		||||
			Hash: *h,
 | 
			
		||||
		})
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return 0, "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if name == "" {
 | 
			
		||||
		name = strings.TrimSuffix(path.Base(u.Path), ".git")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return TypeDir, name, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Update uses git to pull the repository and update it
 | 
			
		||||
// to the latest revision. It allows specifying the depth
 | 
			
		||||
// and recursion options via query string. It returns
 | 
			
		||||
// true if update was successful and false if the
 | 
			
		||||
// repository is already up-to-date
 | 
			
		||||
func (GitDownloader) Update(opts Options) (bool, error) {
 | 
			
		||||
	u, err := url.Parse(opts.URL)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
	u.Scheme = strings.TrimPrefix(u.Scheme, "git+")
 | 
			
		||||
 | 
			
		||||
	query := u.Query()
 | 
			
		||||
	query.Del("~rev")
 | 
			
		||||
 | 
			
		||||
	depthStr := query.Get("~depth")
 | 
			
		||||
	query.Del("~depth")
 | 
			
		||||
 | 
			
		||||
	recursive := query.Get("~recursive")
 | 
			
		||||
	query.Del("~recursive")
 | 
			
		||||
 | 
			
		||||
	u.RawQuery = query.Encode()
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainOpen(opts.Destination)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w, err := r.Worktree()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	depth := 0
 | 
			
		||||
	if depthStr != "" {
 | 
			
		||||
		depth, err = strconv.Atoi(depthStr)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return false, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	po := &git.PullOptions{
 | 
			
		||||
		Depth:             depth,
 | 
			
		||||
		Progress:          opts.Progress,
 | 
			
		||||
		RecurseSubmodules: git.NoRecurseSubmodules,
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if recursive == "true" {
 | 
			
		||||
		po.RecurseSubmodules = git.DefaultSubmoduleRecursionDepth
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	m, err := getManifest(opts.Destination)
 | 
			
		||||
	manifestOK := err == nil
 | 
			
		||||
 | 
			
		||||
	err = w.Pull(po)
 | 
			
		||||
	if errors.Is(err, git.NoErrAlreadyUpToDate) {
 | 
			
		||||
		return false, nil
 | 
			
		||||
	} else if err != nil {
 | 
			
		||||
		return false, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if manifestOK {
 | 
			
		||||
		err = writeManifest(opts.Destination, m)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return true, err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return true, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										90
									
								
								internal/dl/torrent.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										90
									
								
								internal/dl/torrent.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,90 @@
 | 
			
		||||
package dl
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"regexp"
 | 
			
		||||
	"strings"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	urlMatchRegex       = regexp.MustCompile(`(magnet|torrent\+https?):.*`)
 | 
			
		||||
	ErrAria2NotFound    = errors.New("aria2 must be installed for torrent functionality")
 | 
			
		||||
	ErrDestinationEmpty = errors.New("the destination directory is empty")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type TorrentDownloader struct{}
 | 
			
		||||
 | 
			
		||||
// Name always returns "file"
 | 
			
		||||
func (TorrentDownloader) Name() string {
 | 
			
		||||
	return "torrent"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// MatchURL returns true if the URL is a magnet link
 | 
			
		||||
// or an http(s) link with a "torrent+" prefix
 | 
			
		||||
func (TorrentDownloader) MatchURL(u string) bool {
 | 
			
		||||
	return urlMatchRegex.MatchString(u)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Download downloads a file over the BitTorrent protocol.
 | 
			
		||||
func (TorrentDownloader) Download(opts Options) (Type, string, error) {
 | 
			
		||||
	aria2Path, err := exec.LookPath("aria2c")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", ErrAria2NotFound
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	opts.URL = strings.TrimPrefix(opts.URL, "torrent+")
 | 
			
		||||
 | 
			
		||||
	cmd := exec.Command(aria2Path, "--summary-interval=0", "--log-level=warn", "--seed-time=0", "--dir="+opts.Destination, opts.URL)
 | 
			
		||||
	cmd.Stdout = os.Stdout
 | 
			
		||||
	cmd.Stderr = os.Stderr
 | 
			
		||||
	err = cmd.Run()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", fmt.Errorf("aria2c returned an error: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = removeTorrentFiles(opts.Destination)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return determineType(opts.Destination)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removeTorrentFiles(path string) error {
 | 
			
		||||
	filePaths, err := filepath.Glob(filepath.Join(path, "*.torrent"))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, filePath := range filePaths {
 | 
			
		||||
		err = os.Remove(filePath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func determineType(path string) (Type, string, error) {
 | 
			
		||||
	files, err := os.ReadDir(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0, "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(files) > 1 {
 | 
			
		||||
		return TypeDir, "", nil
 | 
			
		||||
	} else if len(files) == 1 {
 | 
			
		||||
		if files[0].IsDir() {
 | 
			
		||||
			return TypeDir, files[0].Name(), nil
 | 
			
		||||
		} else {
 | 
			
		||||
			return TypeFile, files[0].Name(), nil
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return 0, "", ErrDestinationEmpty
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										93
									
								
								internal/dlcache/dlcache.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								internal/dlcache/dlcache.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package dlcache
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/config"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// BasePath returns the base path of the download cache
 | 
			
		||||
func BasePath(ctx context.Context) string {
 | 
			
		||||
	return filepath.Join(config.GetPaths(ctx).CacheDir, "dl")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new directory with the given ID in the cache.
 | 
			
		||||
// If a directory with the same ID already exists,
 | 
			
		||||
// it will be deleted before creating a new one.
 | 
			
		||||
func New(ctx context.Context, id string) (string, error) {
 | 
			
		||||
	h, err := hashID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	itemPath := filepath.Join(BasePath(ctx), h)
 | 
			
		||||
 | 
			
		||||
	fi, err := os.Stat(itemPath)
 | 
			
		||||
	if err == nil || (fi != nil && !fi.IsDir()) {
 | 
			
		||||
		err = os.RemoveAll(itemPath)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = os.MkdirAll(itemPath, 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return itemPath, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Get checks if an entry with the given ID
 | 
			
		||||
// already exists in the cache, and if so,
 | 
			
		||||
// returns the directory and true. If it
 | 
			
		||||
// does not exist, it returns an empty string
 | 
			
		||||
// and false.
 | 
			
		||||
func Get(ctx context.Context, id string) (string, bool) {
 | 
			
		||||
	h, err := hashID(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
	itemPath := filepath.Join(BasePath(ctx), h)
 | 
			
		||||
 | 
			
		||||
	_, err = os.Stat(itemPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return itemPath, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// hashID hashes the input ID with SHA1
 | 
			
		||||
// and returns the hex string of the hashed
 | 
			
		||||
// ID.
 | 
			
		||||
func hashID(id string) (string, error) {
 | 
			
		||||
	h := sha1.New()
 | 
			
		||||
	_, err := io.WriteString(h, id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil)), nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										76
									
								
								internal/dlcache/dlcache_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								internal/dlcache/dlcache_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,76 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package dlcache_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"crypto/sha1"
 | 
			
		||||
	"encoding/hex"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/config"
 | 
			
		||||
	"lure.sh/lure/internal/dlcache"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	dir, err := os.MkdirTemp("/tmp", "lure-dlcache-test.*")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		panic(err)
 | 
			
		||||
	}
 | 
			
		||||
	config.GetPaths(context.Background()).RepoDir = dir
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestNew(t *testing.T) {
 | 
			
		||||
	const id = "https://example.com"
 | 
			
		||||
	dir, err := dlcache.New(id)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	exp := filepath.Join(dlcache.BasePath(), sha1sum(id))
 | 
			
		||||
	if dir != exp {
 | 
			
		||||
		t.Errorf("Expected %s, got %s", exp, dir)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fi, err := os.Stat(dir)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("stat: expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !fi.IsDir() {
 | 
			
		||||
		t.Errorf("Expected cache item to be a directory")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dir2, ok := dlcache.Get(id)
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Errorf("Expected Get() to return valid value")
 | 
			
		||||
	}
 | 
			
		||||
	if dir2 != dir {
 | 
			
		||||
		t.Errorf("Expected %s from Get(), got %s", dir, dir2)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sha1sum(id string) string {
 | 
			
		||||
	h := sha1.New()
 | 
			
		||||
	_, _ = io.WriteString(h, id)
 | 
			
		||||
	return hex.EncodeToString(h.Sum(nil))
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										110
									
								
								internal/osutils/move.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								internal/osutils/move.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,110 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package osutils
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Move attempts to use os.Rename and if that fails (such as for a cross-device move),
 | 
			
		||||
// it instead copies the source to the destination and then removes the source.
 | 
			
		||||
func Move(sourcePath, destPath string) error {
 | 
			
		||||
	// Try to rename the source to the destination
 | 
			
		||||
	err := os.Rename(sourcePath, destPath)
 | 
			
		||||
	if err == nil {
 | 
			
		||||
		return nil // Successful move
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Rename failed, so copy the source to the destination
 | 
			
		||||
	err = copyDirOrFile(sourcePath, destPath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Copy successful, remove the original source
 | 
			
		||||
	err = os.RemoveAll(sourcePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyDirOrFile(sourcePath, destPath string) error {
 | 
			
		||||
	sourceInfo, err := os.Stat(sourcePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if sourceInfo.IsDir() {
 | 
			
		||||
		return copyDir(sourcePath, destPath, sourceInfo)
 | 
			
		||||
	} else if sourceInfo.Mode().IsRegular() {
 | 
			
		||||
		return copyFile(sourcePath, destPath, sourceInfo)
 | 
			
		||||
	} else {
 | 
			
		||||
		// ignore non-regular files
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyDir(sourcePath, destPath string, sourceInfo os.FileInfo) error {
 | 
			
		||||
	err := os.MkdirAll(destPath, sourceInfo.Mode())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	entries, err := os.ReadDir(sourcePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, entry := range entries {
 | 
			
		||||
		sourceEntry := filepath.Join(sourcePath, entry.Name())
 | 
			
		||||
		destEntry := filepath.Join(destPath, entry.Name())
 | 
			
		||||
 | 
			
		||||
		err = copyDirOrFile(sourceEntry, destEntry)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func copyFile(sourcePath, destPath string, sourceInfo os.FileInfo) error {
 | 
			
		||||
	sourceFile, err := os.Open(sourcePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer sourceFile.Close()
 | 
			
		||||
 | 
			
		||||
	destFile, err := os.OpenFile(destPath, os.O_CREATE|os.O_TRUNC|os.O_RDWR, sourceInfo.Mode())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer destFile.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(destFile, sourceFile)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										223
									
								
								internal/overrides/overrides.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								internal/overrides/overrides.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package overrides
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/cpu"
 | 
			
		||||
	"lure.sh/lure/internal/db"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type Opts struct {
 | 
			
		||||
	Name         string
 | 
			
		||||
	Overrides    bool
 | 
			
		||||
	LikeDistros  bool
 | 
			
		||||
	Languages    []string
 | 
			
		||||
	LanguageTags []language.Tag
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var DefaultOpts = &Opts{
 | 
			
		||||
	Overrides:   true,
 | 
			
		||||
	LikeDistros: true,
 | 
			
		||||
	Languages:   []string{"en"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Resolve generates a slice of possible override names in the order that they should be checked
 | 
			
		||||
func Resolve(info *distro.OSRelease, opts *Opts) ([]string, error) {
 | 
			
		||||
	if opts == nil {
 | 
			
		||||
		opts = DefaultOpts
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !opts.Overrides {
 | 
			
		||||
		return []string{opts.Name}, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	langs, err := parseLangs(opts.Languages, opts.LanguageTags)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	architectures, err := cpu.CompatibleArches(cpu.Arch())
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	distros := []string{info.ID}
 | 
			
		||||
	if opts.LikeDistros {
 | 
			
		||||
		distros = append(distros, info.Like...)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var out []string
 | 
			
		||||
	for _, lang := range langs {
 | 
			
		||||
		for _, distro := range distros {
 | 
			
		||||
			for _, arch := range architectures {
 | 
			
		||||
				out = append(out, opts.Name+"_"+arch+"_"+distro+"_"+lang)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			out = append(out, opts.Name+"_"+distro+"_"+lang)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		for _, arch := range architectures {
 | 
			
		||||
			out = append(out, opts.Name+"_"+arch+"_"+lang)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, opts.Name+"_"+lang)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, distro := range distros {
 | 
			
		||||
		for _, arch := range architectures {
 | 
			
		||||
			out = append(out, opts.Name+"_"+arch+"_"+distro)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		out = append(out, opts.Name+"_"+distro)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, arch := range architectures {
 | 
			
		||||
		out = append(out, opts.Name+"_"+arch)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out = append(out, opts.Name)
 | 
			
		||||
 | 
			
		||||
	for index, item := range out {
 | 
			
		||||
		out[index] = strings.TrimPrefix(strings.ReplaceAll(item, "-", "_"), "_")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithName(name string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Name = name
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithOverrides(v bool) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Overrides = v
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLikeDistros(v bool) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.LikeDistros = v
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLanguages(langs []string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Languages = langs
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (o *Opts) WithLanguageTags(langs []string) *Opts {
 | 
			
		||||
	out := &Opts{}
 | 
			
		||||
	*out = *o
 | 
			
		||||
 | 
			
		||||
	out.Languages = langs
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ResolvedPackage is a LURE package after its overrides
 | 
			
		||||
// have been resolved
 | 
			
		||||
type ResolvedPackage struct {
 | 
			
		||||
	Name          string   `sh:"name"`
 | 
			
		||||
	Version       string   `sh:"version"`
 | 
			
		||||
	Release       int      `sh:"release"`
 | 
			
		||||
	Epoch         uint     `sh:"epoch"`
 | 
			
		||||
	Description   string   `db:"description"`
 | 
			
		||||
	Homepage      string   `db:"homepage"`
 | 
			
		||||
	Maintainer    string   `db:"maintainer"`
 | 
			
		||||
	Architectures []string `sh:"architectures"`
 | 
			
		||||
	Licenses      []string `sh:"license"`
 | 
			
		||||
	Provides      []string `sh:"provides"`
 | 
			
		||||
	Conflicts     []string `sh:"conflicts"`
 | 
			
		||||
	Replaces      []string `sh:"replaces"`
 | 
			
		||||
	Depends       []string `sh:"deps"`
 | 
			
		||||
	BuildDepends  []string `sh:"build_deps"`
 | 
			
		||||
	OptDepends    []string `sh:"opt_deps"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func ResolvePackage(pkg *db.Package, overrides []string) *ResolvedPackage {
 | 
			
		||||
	out := &ResolvedPackage{}
 | 
			
		||||
	outVal := reflect.ValueOf(out).Elem()
 | 
			
		||||
	pkgVal := reflect.ValueOf(pkg).Elem()
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < outVal.NumField(); i++ {
 | 
			
		||||
		fieldVal := outVal.Field(i)
 | 
			
		||||
		fieldType := fieldVal.Type()
 | 
			
		||||
		pkgFieldVal := pkgVal.FieldByName(outVal.Type().Field(i).Name)
 | 
			
		||||
		pkgFieldType := pkgFieldVal.Type()
 | 
			
		||||
 | 
			
		||||
		if strings.HasPrefix(pkgFieldType.String(), "db.JSON") {
 | 
			
		||||
			pkgFieldVal = pkgFieldVal.FieldByName("Val")
 | 
			
		||||
			pkgFieldType = pkgFieldVal.Type()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if pkgFieldType.AssignableTo(fieldType) {
 | 
			
		||||
			fieldVal.Set(pkgFieldVal)
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if pkgFieldVal.Kind() == reflect.Map && pkgFieldType.Elem().AssignableTo(fieldType) {
 | 
			
		||||
			for _, override := range overrides {
 | 
			
		||||
				overrideVal := pkgFieldVal.MapIndex(reflect.ValueOf(override))
 | 
			
		||||
				if !overrideVal.IsValid() {
 | 
			
		||||
					continue
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				fieldVal.Set(overrideVal)
 | 
			
		||||
				break
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func parseLangs(langs []string, tags []language.Tag) ([]string, error) {
 | 
			
		||||
	out := make([]string, len(tags)+len(langs))
 | 
			
		||||
	for i, tag := range tags {
 | 
			
		||||
		base, _ := tag.Base()
 | 
			
		||||
		out[i] = base.String()
 | 
			
		||||
	}
 | 
			
		||||
	for i, lang := range langs {
 | 
			
		||||
		tag, err := language.Parse(lang)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return nil, err
 | 
			
		||||
		}
 | 
			
		||||
		base, _ := tag.Base()
 | 
			
		||||
		out[len(tags)+i] = base.String()
 | 
			
		||||
	}
 | 
			
		||||
	slices.Sort(out)
 | 
			
		||||
	out = slices.Compact(out)
 | 
			
		||||
	return out, nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										195
									
								
								internal/overrides/overrides_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										195
									
								
								internal/overrides/overrides_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,195 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package overrides_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/overrides"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var info = &distro.OSRelease{
 | 
			
		||||
	ID:   "centos",
 | 
			
		||||
	Like: []string{"rhel", "fedora"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolve(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, nil)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos_en",
 | 
			
		||||
		"centos_en",
 | 
			
		||||
		"amd64_rhel_en",
 | 
			
		||||
		"rhel_en",
 | 
			
		||||
		"amd64_fedora_en",
 | 
			
		||||
		"fedora_en",
 | 
			
		||||
		"amd64_en",
 | 
			
		||||
		"en",
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64_rhel",
 | 
			
		||||
		"rhel",
 | 
			
		||||
		"amd64_fedora",
 | 
			
		||||
		"fedora",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveName(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Name:        "deps",
 | 
			
		||||
		Overrides:   true,
 | 
			
		||||
		LikeDistros: true,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"deps_amd64_centos",
 | 
			
		||||
		"deps_centos",
 | 
			
		||||
		"deps_amd64_rhel",
 | 
			
		||||
		"deps_rhel",
 | 
			
		||||
		"deps_amd64_fedora",
 | 
			
		||||
		"deps_fedora",
 | 
			
		||||
		"deps_amd64",
 | 
			
		||||
		"deps",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveArch(t *testing.T) {
 | 
			
		||||
	os.Setenv("LURE_ARCH", "arm7")
 | 
			
		||||
	defer os.Setenv("LURE_ARCH", "")
 | 
			
		||||
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Name:        "deps",
 | 
			
		||||
		Overrides:   true,
 | 
			
		||||
		LikeDistros: true,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"deps_arm7_centos",
 | 
			
		||||
		"deps_arm6_centos",
 | 
			
		||||
		"deps_arm5_centos",
 | 
			
		||||
		"deps_centos",
 | 
			
		||||
		"deps_arm7_rhel",
 | 
			
		||||
		"deps_arm6_rhel",
 | 
			
		||||
		"deps_arm5_rhel",
 | 
			
		||||
		"deps_rhel",
 | 
			
		||||
		"deps_arm7_fedora",
 | 
			
		||||
		"deps_arm6_fedora",
 | 
			
		||||
		"deps_arm5_fedora",
 | 
			
		||||
		"deps_fedora",
 | 
			
		||||
		"deps_arm7",
 | 
			
		||||
		"deps_arm6",
 | 
			
		||||
		"deps_arm5",
 | 
			
		||||
		"deps",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveNoLikeDistros(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Overrides:   true,
 | 
			
		||||
		LikeDistros: false,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveNoOverrides(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Name:        "deps",
 | 
			
		||||
		Overrides:   false,
 | 
			
		||||
		LikeDistros: false,
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{"deps"}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestResolveLangs(t *testing.T) {
 | 
			
		||||
	names, err := overrides.Resolve(info, &overrides.Opts{
 | 
			
		||||
		Overrides:    true,
 | 
			
		||||
		Languages:    []string{"ru_RU", "en", "en_US"},
 | 
			
		||||
		LanguageTags: []language.Tag{language.BritishEnglish},
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := []string{
 | 
			
		||||
		"amd64_centos_en",
 | 
			
		||||
		"centos_en",
 | 
			
		||||
		"amd64_en",
 | 
			
		||||
		"en",
 | 
			
		||||
		"amd64_centos_ru",
 | 
			
		||||
		"centos_ru",
 | 
			
		||||
		"amd64_ru",
 | 
			
		||||
		"ru",
 | 
			
		||||
		"amd64_centos",
 | 
			
		||||
		"centos",
 | 
			
		||||
		"amd64",
 | 
			
		||||
		"",
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(names, expected) {
 | 
			
		||||
		t.Errorf("expected %v, got %v", expected, names)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										37
									
								
								internal/pager/highlighting.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								internal/pager/highlighting.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package pager
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"io"
 | 
			
		||||
 | 
			
		||||
	"github.com/alecthomas/chroma/v2/quick"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func SyntaxHighlightBash(r io.Reader, style string) (string, error) {
 | 
			
		||||
	data, err := io.ReadAll(r)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	w := &bytes.Buffer{}
 | 
			
		||||
	err = quick.Highlight(w, string(data), "bash", "terminal", style)
 | 
			
		||||
	return w.String(), err
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										143
									
								
								internal/pager/pager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										143
									
								
								internal/pager/pager.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,143 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package pager
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/charmbracelet/bubbles/viewport"
 | 
			
		||||
	tea "github.com/charmbracelet/bubbletea"
 | 
			
		||||
	"github.com/charmbracelet/lipgloss"
 | 
			
		||||
	"github.com/muesli/reflow/wordwrap"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	titleStyle lipgloss.Style
 | 
			
		||||
	infoStyle  lipgloss.Style
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	b1 := lipgloss.RoundedBorder()
 | 
			
		||||
	b1.Right = "\u251C"
 | 
			
		||||
	titleStyle = lipgloss.NewStyle().BorderStyle(b1).Padding(0, 1)
 | 
			
		||||
 | 
			
		||||
	b2 := lipgloss.RoundedBorder()
 | 
			
		||||
	b2.Left = "\u2524"
 | 
			
		||||
	infoStyle = titleStyle.Copy().BorderStyle(b2)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Pager struct {
 | 
			
		||||
	model pagerModel
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func New(name, content string) *Pager {
 | 
			
		||||
	return &Pager{
 | 
			
		||||
		model: pagerModel{
 | 
			
		||||
			name:    name,
 | 
			
		||||
			content: content,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (p *Pager) Run() error {
 | 
			
		||||
	prog := tea.NewProgram(
 | 
			
		||||
		p.model,
 | 
			
		||||
		tea.WithMouseCellMotion(),
 | 
			
		||||
		tea.WithAltScreen(),
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	_, err := prog.Run()
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type pagerModel struct {
 | 
			
		||||
	name     string
 | 
			
		||||
	content  string
 | 
			
		||||
	ready    bool
 | 
			
		||||
	viewport viewport.Model
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) Init() tea.Cmd {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
 | 
			
		||||
	var (
 | 
			
		||||
		cmd  tea.Cmd
 | 
			
		||||
		cmds []tea.Cmd
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	switch msg := msg.(type) {
 | 
			
		||||
	case tea.KeyMsg:
 | 
			
		||||
		k := msg.String()
 | 
			
		||||
		if k == "ctrl+c" || k == "q" || k == "esc" {
 | 
			
		||||
			return pm, tea.Quit
 | 
			
		||||
		}
 | 
			
		||||
	case tea.WindowSizeMsg:
 | 
			
		||||
		headerHeight := lipgloss.Height(pm.headerView())
 | 
			
		||||
		footerHeight := lipgloss.Height(pm.footerView())
 | 
			
		||||
		verticalMarginHeight := headerHeight + footerHeight
 | 
			
		||||
 | 
			
		||||
		if !pm.ready {
 | 
			
		||||
			pm.viewport = viewport.New(msg.Width, msg.Height-verticalMarginHeight)
 | 
			
		||||
			pm.viewport.HighPerformanceRendering = true
 | 
			
		||||
			pm.viewport.YPosition = headerHeight + 1
 | 
			
		||||
			pm.viewport.SetContent(wordwrap.String(pm.content, msg.Width))
 | 
			
		||||
			pm.ready = true
 | 
			
		||||
		} else {
 | 
			
		||||
			pm.viewport.Width = msg.Width
 | 
			
		||||
			pm.viewport.Height = msg.Height - verticalMarginHeight
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		cmds = append(cmds, viewport.Sync(pm.viewport))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Handle keyboard and mouse events in the viewport
 | 
			
		||||
	pm.viewport, cmd = pm.viewport.Update(msg)
 | 
			
		||||
	cmds = append(cmds, cmd)
 | 
			
		||||
 | 
			
		||||
	return pm, tea.Batch(cmds...)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) View() string {
 | 
			
		||||
	if !pm.ready {
 | 
			
		||||
		return "\n  Initializing..."
 | 
			
		||||
	}
 | 
			
		||||
	return fmt.Sprintf("%s\n%s\n%s", pm.headerView(), pm.viewport.View(), pm.footerView())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) headerView() string {
 | 
			
		||||
	title := titleStyle.Render(pm.name)
 | 
			
		||||
	line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(title)))
 | 
			
		||||
	return lipgloss.JoinHorizontal(lipgloss.Center, title, line)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (pm pagerModel) footerView() string {
 | 
			
		||||
	info := infoStyle.Render(fmt.Sprintf("%3.f%%", pm.viewport.ScrollPercent()*100))
 | 
			
		||||
	line := strings.Repeat("─", max(0, pm.viewport.Width-lipgloss.Width(info)))
 | 
			
		||||
	return lipgloss.JoinHorizontal(lipgloss.Center, line, info)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func max(a, b int) int {
 | 
			
		||||
	if a > b {
 | 
			
		||||
		return a
 | 
			
		||||
	}
 | 
			
		||||
	return b
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										223
									
								
								internal/shutils/decoder/decoder.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										223
									
								
								internal/shutils/decoder/decoder.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,223 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package decoder
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
 | 
			
		||||
	"github.com/mitchellh/mapstructure"
 | 
			
		||||
	"lure.sh/lure/internal/overrides"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var ErrNotPointerToStruct = errors.New("val must be a pointer to a struct")
 | 
			
		||||
 | 
			
		||||
type VarNotFoundError struct {
 | 
			
		||||
	name string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (nfe VarNotFoundError) Error() string {
 | 
			
		||||
	return "required variable '" + nfe.name + "' could not be found"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type InvalidTypeError struct {
 | 
			
		||||
	name    string
 | 
			
		||||
	vartype string
 | 
			
		||||
	exptype string
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (ite InvalidTypeError) Error() string {
 | 
			
		||||
	return "variable '" + ite.name + "' is of type " + ite.vartype + ", but " + ite.exptype + " is expected"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Decoder provides methods for decoding variable values
 | 
			
		||||
type Decoder struct {
 | 
			
		||||
	info   *distro.OSRelease
 | 
			
		||||
	Runner *interp.Runner
 | 
			
		||||
	// Enable distro overrides (true by default)
 | 
			
		||||
	Overrides bool
 | 
			
		||||
	// Enable using like distros for overrides
 | 
			
		||||
	LikeDistros bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New creates a new variable decoder
 | 
			
		||||
func New(info *distro.OSRelease, runner *interp.Runner) *Decoder {
 | 
			
		||||
	return &Decoder{info, runner, true, len(info.Like) > 0}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DecodeVar decodes a variable to val using reflection.
 | 
			
		||||
// Structs should use the "sh" struct tag.
 | 
			
		||||
func (d *Decoder) DecodeVar(name string, val any) error {
 | 
			
		||||
	variable := d.getVar(name)
 | 
			
		||||
	if variable == nil {
 | 
			
		||||
		return VarNotFoundError{name}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
 | 
			
		||||
		WeaklyTypedInput: true,
 | 
			
		||||
		DecodeHook: mapstructure.DecodeHookFuncValue(func(from, to reflect.Value) (interface{}, error) {
 | 
			
		||||
			if strings.Contains(to.Type().String(), "db.JSON") {
 | 
			
		||||
				valType := to.FieldByName("Val").Type()
 | 
			
		||||
				if !from.Type().AssignableTo(valType) {
 | 
			
		||||
					return nil, InvalidTypeError{name, from.Type().String(), valType.String()}
 | 
			
		||||
				}
 | 
			
		||||
 | 
			
		||||
				to.FieldByName("Val").Set(from)
 | 
			
		||||
				return to, nil
 | 
			
		||||
			}
 | 
			
		||||
			return from.Interface(), nil
 | 
			
		||||
		}),
 | 
			
		||||
		Result:  val,
 | 
			
		||||
		TagName: "sh",
 | 
			
		||||
	})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch variable.Kind {
 | 
			
		||||
	case expand.Indexed:
 | 
			
		||||
		return dec.Decode(variable.List)
 | 
			
		||||
	case expand.Associative:
 | 
			
		||||
		return dec.Decode(variable.Map)
 | 
			
		||||
	default:
 | 
			
		||||
		return dec.Decode(variable.Str)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DecodeVars decodes all variables to val using reflection.
 | 
			
		||||
// Structs should use the "sh" struct tag.
 | 
			
		||||
func (d *Decoder) DecodeVars(val any) error {
 | 
			
		||||
	valKind := reflect.TypeOf(val).Kind()
 | 
			
		||||
	if valKind != reflect.Pointer {
 | 
			
		||||
		return ErrNotPointerToStruct
 | 
			
		||||
	} else {
 | 
			
		||||
		elemKind := reflect.TypeOf(val).Elem().Kind()
 | 
			
		||||
		if elemKind != reflect.Struct {
 | 
			
		||||
			return ErrNotPointerToStruct
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	rVal := reflect.ValueOf(val).Elem()
 | 
			
		||||
 | 
			
		||||
	for i := 0; i < rVal.NumField(); i++ {
 | 
			
		||||
		field := rVal.Field(i)
 | 
			
		||||
		fieldType := rVal.Type().Field(i)
 | 
			
		||||
 | 
			
		||||
		if !fieldType.IsExported() {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		name := fieldType.Name
 | 
			
		||||
		tag := fieldType.Tag.Get("sh")
 | 
			
		||||
		required := false
 | 
			
		||||
		if tag != "" {
 | 
			
		||||
			if strings.Contains(tag, ",") {
 | 
			
		||||
				splitTag := strings.Split(tag, ",")
 | 
			
		||||
				name = splitTag[0]
 | 
			
		||||
 | 
			
		||||
				if len(splitTag) > 1 {
 | 
			
		||||
					if slices.Contains(splitTag, "required") {
 | 
			
		||||
						required = true
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			} else {
 | 
			
		||||
				name = tag
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		newVal := reflect.New(field.Type())
 | 
			
		||||
		err := d.DecodeVar(name, newVal.Interface())
 | 
			
		||||
		if _, ok := err.(VarNotFoundError); ok && !required {
 | 
			
		||||
			continue
 | 
			
		||||
		} else if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		field.Set(newVal.Elem())
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ScriptFunc func(ctx context.Context, opts ...interp.RunnerOption) error
 | 
			
		||||
 | 
			
		||||
// GetFunc returns a function corresponding to a bash function
 | 
			
		||||
// with the given name
 | 
			
		||||
func (d *Decoder) GetFunc(name string) (ScriptFunc, bool) {
 | 
			
		||||
	fn := d.getFunc(name)
 | 
			
		||||
	if fn == nil {
 | 
			
		||||
		return nil, false
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return func(ctx context.Context, opts ...interp.RunnerOption) error {
 | 
			
		||||
		sub := d.Runner.Subshell()
 | 
			
		||||
		for _, opt := range opts {
 | 
			
		||||
			opt(sub)
 | 
			
		||||
		}
 | 
			
		||||
		return sub.Run(ctx, fn)
 | 
			
		||||
	}, true
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (d *Decoder) getFunc(name string) *syntax.Stmt {
 | 
			
		||||
	names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, fnName := range names {
 | 
			
		||||
		fn, ok := d.Runner.Funcs[fnName]
 | 
			
		||||
		if ok {
 | 
			
		||||
			return fn
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// getVar gets a variable based on its name, taking into account
 | 
			
		||||
// override variables and nameref variables.
 | 
			
		||||
func (d *Decoder) getVar(name string) *expand.Variable {
 | 
			
		||||
	names, err := overrides.Resolve(d.info, overrides.DefaultOpts.WithName(name))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, varName := range names {
 | 
			
		||||
		val, ok := d.Runner.Vars[varName]
 | 
			
		||||
		if ok {
 | 
			
		||||
			// Resolve nameref variables
 | 
			
		||||
			_, resolved := val.Resolve(expand.FuncEnviron(func(s string) string {
 | 
			
		||||
				if val, ok := d.Runner.Vars[s]; ok {
 | 
			
		||||
					return val.String()
 | 
			
		||||
				}
 | 
			
		||||
				return ""
 | 
			
		||||
			}))
 | 
			
		||||
			val = resolved
 | 
			
		||||
 | 
			
		||||
			return &val
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										224
									
								
								internal/shutils/decoder/decoder_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,224 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package decoder_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"errors"
 | 
			
		||||
	"os"
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/decoder"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type BuildVars struct {
 | 
			
		||||
	Name          string   `sh:"name,required"`
 | 
			
		||||
	Version       string   `sh:"version,required"`
 | 
			
		||||
	Release       int      `sh:"release,required"`
 | 
			
		||||
	Epoch         uint     `sh:"epoch"`
 | 
			
		||||
	Description   string   `sh:"desc"`
 | 
			
		||||
	Homepage      string   `sh:"homepage"`
 | 
			
		||||
	Maintainer    string   `sh:"maintainer"`
 | 
			
		||||
	Architectures []string `sh:"architectures"`
 | 
			
		||||
	Licenses      []string `sh:"license"`
 | 
			
		||||
	Provides      []string `sh:"provides"`
 | 
			
		||||
	Conflicts     []string `sh:"conflicts"`
 | 
			
		||||
	Depends       []string `sh:"deps"`
 | 
			
		||||
	BuildDepends  []string `sh:"build_deps"`
 | 
			
		||||
	Replaces      []string `sh:"replaces"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.arsenm.dev'
 | 
			
		||||
	maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
	architectures=('arm64' 'amd64')
 | 
			
		||||
	license=('GPL-3.0-or-later')
 | 
			
		||||
	provides=('test')
 | 
			
		||||
	conflicts=('test')
 | 
			
		||||
	replaces=('test-old')
 | 
			
		||||
	replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
	deps=('sudo')
 | 
			
		||||
 | 
			
		||||
	build_deps=('golang')
 | 
			
		||||
	build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
	test() {
 | 
			
		||||
		echo "Test"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVars(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	expected := BuildVars{
 | 
			
		||||
		Name:          "test",
 | 
			
		||||
		Version:       "0.0.1",
 | 
			
		||||
		Release:       1,
 | 
			
		||||
		Epoch:         2,
 | 
			
		||||
		Description:   "Test package",
 | 
			
		||||
		Homepage:      "https://lure.arsenm.dev",
 | 
			
		||||
		Maintainer:    "Arsen Musayelyan <arsen@arsenm.dev>",
 | 
			
		||||
		Architectures: []string{"arm64", "amd64"},
 | 
			
		||||
		Licenses:      []string{"GPL-3.0-or-later"},
 | 
			
		||||
		Provides:      []string{"test"},
 | 
			
		||||
		Conflicts:     []string{"test"},
 | 
			
		||||
		Replaces:      []string{"test-legacy"},
 | 
			
		||||
		Depends:       []string{"sudo"},
 | 
			
		||||
		BuildDepends:  []string{"go"},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !reflect.DeepEqual(bv, expected) {
 | 
			
		||||
		t.Errorf("Expected %v, got %v", expected, bv)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestDecodeVarsMissing(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	const testScript = `
 | 
			
		||||
		name='test'
 | 
			
		||||
		epoch=2
 | 
			
		||||
		desc="Test package"
 | 
			
		||||
		homepage='https://lure.arsenm.dev'
 | 
			
		||||
		maintainer='Arsen Musayelyan <arsen@arsenm.dev>'
 | 
			
		||||
		architectures=('arm64' 'amd64')
 | 
			
		||||
		license=('GPL-3.0-or-later')
 | 
			
		||||
		provides=('test')
 | 
			
		||||
		conflicts=('test')
 | 
			
		||||
		replaces=('test-old')
 | 
			
		||||
		replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
		deps=('sudo')
 | 
			
		||||
 | 
			
		||||
		build_deps=('golang')
 | 
			
		||||
		build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
		test() {
 | 
			
		||||
			echo "Test"
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		package() {
 | 
			
		||||
			install-binary test
 | 
			
		||||
		}
 | 
			
		||||
	`
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
 | 
			
		||||
	var bv BuildVars
 | 
			
		||||
	err = dec.DecodeVars(&bv)
 | 
			
		||||
 | 
			
		||||
	var notFoundErr decoder.VarNotFoundError
 | 
			
		||||
	if !errors.As(err, ¬FoundErr) {
 | 
			
		||||
		t.Fatalf("Expected VarNotFoundError, got %T %v", err, err)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestGetFunc(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
	fn, ok := dec.GetFunc("test")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected test() function to exist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	err = fn(ctx, interp.StdIO(os.Stdin, buf, buf))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "Test\n" {
 | 
			
		||||
		t.Fatalf(`Expected "Test\n", got %#v`, buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								internal/shutils/handlers/exec.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,63 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func InsufficientArgsError(cmd string, exp, got int) error {
 | 
			
		||||
	argsWord := "arguments"
 | 
			
		||||
	if exp == 1 {
 | 
			
		||||
		argsWord = "argument"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return fmt.Errorf("%s: command requires at least %d %s, got %d", cmd, exp, argsWord, got)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ExecFunc func(hc interp.HandlerContext, name string, args []string) error
 | 
			
		||||
 | 
			
		||||
type ExecFuncs map[string]ExecFunc
 | 
			
		||||
 | 
			
		||||
// ExecHandler returns a new ExecHandlerFunc that falls back to fallback
 | 
			
		||||
// if the command cannot be found in the map. If fallback is nil, the default
 | 
			
		||||
// handler is used.
 | 
			
		||||
func (ef ExecFuncs) ExecHandler(fallback interp.ExecHandlerFunc) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		name := args[0]
 | 
			
		||||
 | 
			
		||||
		if fn, ok := ef[name]; ok {
 | 
			
		||||
			hctx := interp.HandlerCtx(ctx)
 | 
			
		||||
			if len(args) > 1 {
 | 
			
		||||
				return fn(hctx, args[0], args[1:])
 | 
			
		||||
			} else {
 | 
			
		||||
				return fn(hctx, args[0], nil)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if fallback == nil {
 | 
			
		||||
			fallback = interp.DefaultExecHandler(2 * time.Second)
 | 
			
		||||
		}
 | 
			
		||||
		return fallback(ctx, args)
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										124
									
								
								internal/shutils/handlers/exec_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,124 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package handlers_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"lure.sh/lure/internal/shutils/decoder"
 | 
			
		||||
	"lure.sh/lure/pkg/distro"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
const testScript = `
 | 
			
		||||
	name='test'
 | 
			
		||||
	version='0.0.1'
 | 
			
		||||
	release=1
 | 
			
		||||
	epoch=2
 | 
			
		||||
	desc="Test package"
 | 
			
		||||
	homepage='https://lure.sh'
 | 
			
		||||
	maintainer='Elara Musayelyan <elara@elara.ws>'
 | 
			
		||||
	architectures=('arm64' 'amd64')
 | 
			
		||||
	license=('GPL-3.0-or-later')
 | 
			
		||||
	provides=('test')
 | 
			
		||||
	conflicts=('test')
 | 
			
		||||
	replaces=('test-old')
 | 
			
		||||
	replaces_test_os=('test-legacy')
 | 
			
		||||
 | 
			
		||||
	deps=('sudo')
 | 
			
		||||
 | 
			
		||||
	build_deps=('golang')
 | 
			
		||||
	build_deps_arch=('go')
 | 
			
		||||
 | 
			
		||||
	test() {
 | 
			
		||||
		test-cmd "Hello, World"
 | 
			
		||||
		test-fb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	package() {
 | 
			
		||||
		install-binary test
 | 
			
		||||
	}
 | 
			
		||||
`
 | 
			
		||||
 | 
			
		||||
var osRelease = &distro.OSRelease{
 | 
			
		||||
	ID:   "test_os",
 | 
			
		||||
	Like: []string{"arch"},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestExecFuncs(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(testScript), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	runner, err := interp.New()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dec := decoder.New(osRelease, runner)
 | 
			
		||||
	fn, ok := dec.GetFunc("test")
 | 
			
		||||
	if !ok {
 | 
			
		||||
		t.Fatalf("Expected test() function to exist")
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	eh := shutils.ExecFuncs{
 | 
			
		||||
		"test-cmd": func(hc interp.HandlerContext, name string, args []string) error {
 | 
			
		||||
			if name != "test-cmd" {
 | 
			
		||||
				t.Errorf("Expected name to be 'test-cmd', got '%s'", name)
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if len(args) < 1 {
 | 
			
		||||
				t.Fatalf("Expected at least one argument, got %d", len(args))
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			if args[0] != "Hello, World" {
 | 
			
		||||
				t.Errorf("Expected first argument to be 'Hello, World', got '%s'", args[0])
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			return nil
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	fbInvoked := false
 | 
			
		||||
	fbHandler := func(context.Context, []string) error {
 | 
			
		||||
		fbInvoked = true
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = fn(ctx, interp.ExecHandler(eh.ExecHandler(fbHandler)))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Errorf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if !fbInvoked {
 | 
			
		||||
		t.Errorf("Expected fallback handler to be invoked")
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										114
									
								
								internal/shutils/handlers/fakeroot.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,114 @@
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"os"
 | 
			
		||||
	"os/exec"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/fakeroot"
 | 
			
		||||
	"mvdan.cc/sh/v3/expand"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// FakerootExecHandler was extracted from github.com/mvdan/sh/interp/handler.go
 | 
			
		||||
// and modified to run commands in a fakeroot environent.
 | 
			
		||||
func FakerootExecHandler(killTimeout time.Duration) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		hc := interp.HandlerCtx(ctx)
 | 
			
		||||
		path, err := interp.LookPathDir(hc.Dir, hc.Env, args[0])
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			fmt.Fprintln(hc.Stderr, err)
 | 
			
		||||
			return interp.NewExitStatus(127)
 | 
			
		||||
		}
 | 
			
		||||
		cmd := &exec.Cmd{
 | 
			
		||||
			Path:   path,
 | 
			
		||||
			Args:   args,
 | 
			
		||||
			Env:    execEnv(hc.Env),
 | 
			
		||||
			Dir:    hc.Dir,
 | 
			
		||||
			Stdin:  hc.Stdin,
 | 
			
		||||
			Stdout: hc.Stdout,
 | 
			
		||||
			Stderr: hc.Stderr,
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = fakeroot.Apply(cmd)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err = cmd.Start()
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			if done := ctx.Done(); done != nil {
 | 
			
		||||
				go func() {
 | 
			
		||||
					<-done
 | 
			
		||||
 | 
			
		||||
					if killTimeout <= 0 || runtime.GOOS == "windows" {
 | 
			
		||||
						_ = cmd.Process.Signal(os.Kill)
 | 
			
		||||
						return
 | 
			
		||||
					}
 | 
			
		||||
 | 
			
		||||
					// TODO: don't temporarily leak this goroutine
 | 
			
		||||
					// if the program stops itself with the
 | 
			
		||||
					// interrupt.
 | 
			
		||||
					go func() {
 | 
			
		||||
						time.Sleep(killTimeout)
 | 
			
		||||
						_ = cmd.Process.Signal(os.Kill)
 | 
			
		||||
					}()
 | 
			
		||||
					_ = cmd.Process.Signal(os.Interrupt)
 | 
			
		||||
				}()
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			err = cmd.Wait()
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		switch x := err.(type) {
 | 
			
		||||
		case *exec.ExitError:
 | 
			
		||||
			// started, but errored - default to 1 if OS
 | 
			
		||||
			// doesn't have exit statuses
 | 
			
		||||
			if status, ok := x.Sys().(syscall.WaitStatus); ok {
 | 
			
		||||
				if status.Signaled() {
 | 
			
		||||
					if ctx.Err() != nil {
 | 
			
		||||
						return ctx.Err()
 | 
			
		||||
					}
 | 
			
		||||
					return interp.NewExitStatus(uint8(128 + status.Signal()))
 | 
			
		||||
				}
 | 
			
		||||
				return interp.NewExitStatus(uint8(status.ExitStatus()))
 | 
			
		||||
			}
 | 
			
		||||
			return interp.NewExitStatus(1)
 | 
			
		||||
		case *exec.Error:
 | 
			
		||||
			// did not start
 | 
			
		||||
			fmt.Fprintf(hc.Stderr, "%v\n", err)
 | 
			
		||||
			return interp.NewExitStatus(127)
 | 
			
		||||
		default:
 | 
			
		||||
			return err
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// execEnv was extracted from github.com/mvdan/sh/interp/vars.go
 | 
			
		||||
func execEnv(env expand.Environ) []string {
 | 
			
		||||
	list := make([]string, 0, 64)
 | 
			
		||||
	env.Each(func(name string, vr expand.Variable) bool {
 | 
			
		||||
		if !vr.IsSet() {
 | 
			
		||||
			// If a variable is set globally but unset in the
 | 
			
		||||
			// runner, we need to ensure it's not part of the final
 | 
			
		||||
			// list. Seems like zeroing the element is enough.
 | 
			
		||||
			// This is a linear search, but this scenario should be
 | 
			
		||||
			// rare, and the number of variables shouldn't be large.
 | 
			
		||||
			for i, kv := range list {
 | 
			
		||||
				if strings.HasPrefix(kv, name+"=") {
 | 
			
		||||
					list[i] = ""
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		if vr.Exported && vr.Kind == expand.String {
 | 
			
		||||
			list = append(list, name+"="+vr.String())
 | 
			
		||||
		}
 | 
			
		||||
		return true
 | 
			
		||||
	})
 | 
			
		||||
	return list
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								internal/shutils/handlers/nop.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func NopReadDir(context.Context, string) ([]os.FileInfo, error) {
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopStat(context.Context, string, bool) (os.FileInfo, error) {
 | 
			
		||||
	return nil, os.ErrNotExist
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopExec(context.Context, []string) error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NopOpen(context.Context, string, int, os.FileMode) (io.ReadWriteCloser, error) {
 | 
			
		||||
	return NopRWC{}, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type NopRWC struct{}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Read([]byte) (int, error) {
 | 
			
		||||
	return 0, io.EOF
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Write(b []byte) (int, error) {
 | 
			
		||||
	return len(b), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (NopRWC) Close() error {
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								internal/shutils/handlers/nop_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,58 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package handlers_test
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"context"
 | 
			
		||||
	"os"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
	"mvdan.cc/sh/v3/syntax"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func TestNopExec(t *testing.T) {
 | 
			
		||||
	ctx := context.Background()
 | 
			
		||||
 | 
			
		||||
	fl, err := syntax.NewParser().Parse(strings.NewReader(`/bin/echo test`), "lure.sh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	buf := &bytes.Buffer{}
 | 
			
		||||
	runner, err := interp.New(
 | 
			
		||||
		interp.ExecHandler(handlers.NopExec),
 | 
			
		||||
		interp.StdIO(os.Stdin, buf, buf),
 | 
			
		||||
	)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = runner.Run(ctx, fl)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		t.Fatalf("Expected no error, got %s", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if buf.String() != "" {
 | 
			
		||||
		t.Fatalf("Expected empty string, got %#v", buf.String())
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								internal/shutils/handlers/restricted.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package handlers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"io"
 | 
			
		||||
	"io/fs"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func RestrictedReadDir(allowedPrefixes ...string) interp.ReadDirHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string) ([]fs.FileInfo, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultReadDirHandler()(ctx, s)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, fs.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedStat(allowedPrefixes ...string) interp.StatHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, b bool) (fs.FileInfo, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultStatHandler()(ctx, s, b)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil, fs.ErrNotExist
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedOpen(allowedPrefixes ...string) interp.OpenHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, s string, i int, fm fs.FileMode) (io.ReadWriteCloser, error) {
 | 
			
		||||
		path := filepath.Clean(s)
 | 
			
		||||
		for _, allowedPrefix := range allowedPrefixes {
 | 
			
		||||
			if strings.HasPrefix(path, allowedPrefix) {
 | 
			
		||||
				return interp.DefaultOpenHandler()(ctx, s, i, fm)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return NopRWC{}, nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func RestrictedExec(allowedCmds ...string) interp.ExecHandlerFunc {
 | 
			
		||||
	return func(ctx context.Context, args []string) error {
 | 
			
		||||
		if slices.Contains(allowedCmds, args[0]) {
 | 
			
		||||
			return interp.DefaultExecHandler(2*time.Second)(ctx, args)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										284
									
								
								internal/shutils/helpers/helpers.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										284
									
								
								internal/shutils/helpers/helpers.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,284 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package helpers
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"errors"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"unsafe"
 | 
			
		||||
 | 
			
		||||
	"github.com/go-git/go-git/v5"
 | 
			
		||||
	"github.com/go-git/go-git/v5/plumbing/object"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
	"lure.sh/lure/internal/shutils/handlers"
 | 
			
		||||
	"mvdan.cc/sh/v3/interp"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	ErrNoPipe         = errors.New("command requires data to be piped in")
 | 
			
		||||
	ErrNoDetectManNum = errors.New("manual number cannot be detected from the filename")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// Helpers contains all the helper commands
 | 
			
		||||
var Helpers = handlers.ExecFuncs{
 | 
			
		||||
	"install-binary":       installHelperCmd("/usr/bin", 0o755),
 | 
			
		||||
	"install-systemd-user": installHelperCmd("/usr/lib/systemd/user", 0o644),
 | 
			
		||||
	"install-systemd":      installHelperCmd("/usr/lib/systemd/system", 0o644),
 | 
			
		||||
	"install-config":       installHelperCmd("/etc", 0o644),
 | 
			
		||||
	"install-license":      installHelperCmd("/usr/share/licenses", 0o644),
 | 
			
		||||
	"install-desktop":      installHelperCmd("/usr/share/applications", 0o644),
 | 
			
		||||
	"install-icon":         installHelperCmd("/usr/share/pixmaps", 0o644),
 | 
			
		||||
	"install-manual":       installManualCmd,
 | 
			
		||||
	"install-completion":   installCompletionCmd,
 | 
			
		||||
	"install-library":      installLibraryCmd,
 | 
			
		||||
	"git-version":          gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Restricted contains restricted read-only helper commands
 | 
			
		||||
// that don't modify any state
 | 
			
		||||
var Restricted = handlers.ExecFuncs{
 | 
			
		||||
	"git-version": gitVersionCmd,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installHelperCmd(prefix string, perms os.FileMode) handlers.ExecFunc {
 | 
			
		||||
	return func(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
		if len(args) < 1 {
 | 
			
		||||
			return handlers.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		from := resolvePath(hc, args[0])
 | 
			
		||||
		to := ""
 | 
			
		||||
		if len(args) > 1 {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, args[1])
 | 
			
		||||
		} else {
 | 
			
		||||
			to = filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		err := helperInstall(from, to, perms)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			return fmt.Errorf("%s: %w", cmd, err)
 | 
			
		||||
		}
 | 
			
		||||
		return nil
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installManualCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	if len(args) < 1 {
 | 
			
		||||
		return handlers.InsufficientArgsError(cmd, 1, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	from := resolvePath(hc, args[0])
 | 
			
		||||
	number := filepath.Base(from)
 | 
			
		||||
	// The man page may be compressed with gzip.
 | 
			
		||||
	// If it is, the .gz extension must be removed to properly
 | 
			
		||||
	// detect the number at the end of the filename.
 | 
			
		||||
	number = strings.TrimSuffix(number, ".gz")
 | 
			
		||||
	number = strings.TrimPrefix(filepath.Ext(number), ".")
 | 
			
		||||
 | 
			
		||||
	// If number is not actually a number, return an error
 | 
			
		||||
	if _, err := strconv.Atoi(number); err != nil {
 | 
			
		||||
		return fmt.Errorf("install-manual: %w", ErrNoDetectManNum)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	prefix := "/usr/share/man/man" + number
 | 
			
		||||
	to := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, filepath.Base(from))
 | 
			
		||||
 | 
			
		||||
	return helperInstall(from, to, 0o644)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installCompletionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	// If the command's stdin is the same as the system's,
 | 
			
		||||
	// that means nothing was piped in. In this case, return an error.
 | 
			
		||||
	if hc.Stdin == os.Stdin {
 | 
			
		||||
		return fmt.Errorf("install-completion: %w", ErrNoPipe)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(args) < 2 {
 | 
			
		||||
		return handlers.InsufficientArgsError(cmd, 2, len(args))
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	shell := args[0]
 | 
			
		||||
	name := args[1]
 | 
			
		||||
 | 
			
		||||
	var prefix string
 | 
			
		||||
	switch shell {
 | 
			
		||||
	case "bash":
 | 
			
		||||
		prefix = "/usr/share/bash-completion/completions"
 | 
			
		||||
	case "zsh":
 | 
			
		||||
		prefix = "/usr/share/zsh/site-functions"
 | 
			
		||||
		name = "_" + name
 | 
			
		||||
	case "fish":
 | 
			
		||||
		prefix = "/usr/share/fish/vendor_completions.d"
 | 
			
		||||
		name += ".fish"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	path := filepath.Join(hc.Env.Get("pkgdir").Str, prefix, name)
 | 
			
		||||
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(path), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(path, os.O_TRUNC|os.O_CREATE|os.O_RDWR, 0o644)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, hc.Stdin)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func installLibraryCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	prefix := getLibPrefix(hc)
 | 
			
		||||
	fn := installHelperCmd(prefix, 0o755)
 | 
			
		||||
	return fn(hc, cmd, args)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// See https://wiki.debian.org/Multiarch/Tuples
 | 
			
		||||
var multiarchTupleMap = map[string]string{
 | 
			
		||||
	"386":      "i386-linux-gnu",
 | 
			
		||||
	"amd64":    "x86_64-linux-gnu",
 | 
			
		||||
	"arm5":     "arm-linux-gnueabi",
 | 
			
		||||
	"arm6":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm7":     "arm-linux-gnueabihf",
 | 
			
		||||
	"arm64":    "aarch64-linux-gnu",
 | 
			
		||||
	"mips":     "mips-linux-gnu",
 | 
			
		||||
	"mipsle":   "mipsel-linux-gnu",
 | 
			
		||||
	"mips64":   "mips64-linux-gnuabi64",
 | 
			
		||||
	"mips64le": "mips64el-linux-gnuabi64",
 | 
			
		||||
	"ppc64":    "powerpc64-linux-gnu",
 | 
			
		||||
	"ppc64le":  "powerpc64le-linux-gnu",
 | 
			
		||||
	"s390x":    "s390x-linux-gnu",
 | 
			
		||||
	"riscv64":  "riscv64-linux-gnu",
 | 
			
		||||
	"loong64":  "loongarch64-linux-gnu",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// usrLibDistros is a list of distros that don't support
 | 
			
		||||
// /usr/lib64, and must use /usr/lib
 | 
			
		||||
var usrLibDistros = []string{
 | 
			
		||||
	"arch",
 | 
			
		||||
	"alpine",
 | 
			
		||||
	"void",
 | 
			
		||||
	"chimera",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Based on CMake's GNUInstallDirs
 | 
			
		||||
func getLibPrefix(hc interp.HandlerContext) string {
 | 
			
		||||
	if dir, ok := os.LookupEnv("LURE_LIB_DIR"); ok {
 | 
			
		||||
		return dir
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	out := "/usr/lib"
 | 
			
		||||
 | 
			
		||||
	distroID := hc.Env.Get("DISTRO_ID").Str
 | 
			
		||||
	distroLike := strings.Split(hc.Env.Get("DISTRO_ID_LIKE").Str, " ")
 | 
			
		||||
 | 
			
		||||
	for _, usrLibDistro := range usrLibDistros {
 | 
			
		||||
		if distroID == usrLibDistro || slices.Contains(distroLike, usrLibDistro) {
 | 
			
		||||
			return out
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	wordSize := unsafe.Sizeof(uintptr(0))
 | 
			
		||||
	if wordSize == 8 {
 | 
			
		||||
		out = "/usr/lib64"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	architecture := hc.Env.Get("ARCH").Str
 | 
			
		||||
 | 
			
		||||
	if distroID == "debian" || slices.Contains(distroLike, "debian") ||
 | 
			
		||||
		distroID == "ubuntu" || slices.Contains(distroLike, "ubuntu") {
 | 
			
		||||
 | 
			
		||||
		tuple, ok := multiarchTupleMap[architecture]
 | 
			
		||||
		if ok {
 | 
			
		||||
			out = filepath.Join("/usr/lib", tuple)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return out
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func gitVersionCmd(hc interp.HandlerContext, cmd string, args []string) error {
 | 
			
		||||
	path := hc.Dir
 | 
			
		||||
	if len(args) > 0 {
 | 
			
		||||
		path = resolvePath(hc, args[0])
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	r, err := git.PlainOpen(path)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	revNum := 0
 | 
			
		||||
	commits, err := r.Log(&git.LogOptions{})
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commits.ForEach(func(*object.Commit) error {
 | 
			
		||||
		revNum++
 | 
			
		||||
		return nil
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	HEAD, err := r.Head()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return fmt.Errorf("git-version: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hash := HEAD.Hash().String()
 | 
			
		||||
 | 
			
		||||
	fmt.Fprintf(hc.Stdout, "%d.%s\n", revNum, hash[:7])
 | 
			
		||||
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func helperInstall(from, to string, perms os.FileMode) error {
 | 
			
		||||
	err := os.MkdirAll(filepath.Dir(to), 0o755)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	src, err := os.Open(from)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer src.Close()
 | 
			
		||||
 | 
			
		||||
	dst, err := os.OpenFile(to, os.O_TRUNC|os.O_CREATE|os.O_RDWR, perms)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
	defer dst.Close()
 | 
			
		||||
 | 
			
		||||
	_, err = io.Copy(dst, src)
 | 
			
		||||
	return err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func resolvePath(hc interp.HandlerContext, path string) string {
 | 
			
		||||
	if !filepath.IsAbs(path) {
 | 
			
		||||
		return filepath.Join(hc.Dir, path)
 | 
			
		||||
	}
 | 
			
		||||
	return path
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										155
									
								
								internal/translations/files/lure.en.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								internal/translations/files/lure.en.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1228660974
 | 
			
		||||
value = 'Pulling repository'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2779805870
 | 
			
		||||
value = 'Repository up to date'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1433222829
 | 
			
		||||
value = 'Would you like to view the build script for'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2470847050
 | 
			
		||||
value = 'Failed to prompt user to view build script'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 855659503
 | 
			
		||||
value = 'Would you still like to continue?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1997041569
 | 
			
		||||
value = 'User chose not to continue after reading script'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2347700990
 | 
			
		||||
value = 'Building package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2105058868
 | 
			
		||||
value = 'Downloading sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1884485082
 | 
			
		||||
value = 'Downloading source'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1519177982
 | 
			
		||||
value = 'Error building package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2125220917
 | 
			
		||||
value = 'Choose which package(s) to install'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 812531604
 | 
			
		||||
value = 'Error prompting for choice of package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1040982801
 | 
			
		||||
value = 'Updating version'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1014897988
 | 
			
		||||
value = 'Remove build dependencies?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2205430948
 | 
			
		||||
value = 'Installing build dependencies'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2522710805
 | 
			
		||||
value = 'Installing dependencies'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3602138206
 | 
			
		||||
value = 'Error installing package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2235794125
 | 
			
		||||
value = 'Would you like to remove build dependencies?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2562049386
 | 
			
		||||
value = "Your system's CPU architecture doesn't match this package. Do you want to build anyway?"
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 4006393493
 | 
			
		||||
value = 'The checksums array must be the same length as sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3759891273
 | 
			
		||||
value = 'The package() function is required'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1057080231
 | 
			
		||||
value = 'Executing package()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2687735200
 | 
			
		||||
value = 'Executing prepare()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 535572372
 | 
			
		||||
value = 'Executing version()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 436644691
 | 
			
		||||
value = 'Executing build()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1393316459
 | 
			
		||||
value = 'This package is already installed'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1267660189
 | 
			
		||||
value = 'Source can be updated, updating if required'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 21753247
 | 
			
		||||
value = 'Source found in cache, linked to destination'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 257354570
 | 
			
		||||
value = 'Compressing package'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2952487371
 | 
			
		||||
value = 'Building package metadata'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3121791194
 | 
			
		||||
value = 'Running LURE as root is forbidden as it may cause catastrophic damage to your system'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1256604213
 | 
			
		||||
value = 'Waiting for torrent metadata'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 432261354
 | 
			
		||||
value = 'Downloading torrent file'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1579384326
 | 
			
		||||
value = 'name'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3206337475
 | 
			
		||||
value = 'version'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1810056261
 | 
			
		||||
value = 'new'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1602912115
 | 
			
		||||
value = 'source'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2363381545
 | 
			
		||||
value = 'type'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3419504365
 | 
			
		||||
value = 'downloader'
 | 
			
		||||
							
								
								
									
										151
									
								
								internal/translations/files/lure.ru.toml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										151
									
								
								internal/translations/files/lure.ru.toml
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,151 @@
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1228660974
 | 
			
		||||
value = 'Скачивание репозитория'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2779805870
 | 
			
		||||
value = 'Репозиторий уже обновлен'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1433222829
 | 
			
		||||
value = 'Показать скрипт для пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2470847050
 | 
			
		||||
value = 'Не удалось предложить просмотреть скрипт'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 855659503
 | 
			
		||||
value = 'Продолжить?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1997041569
 | 
			
		||||
value = 'Пользователь решил не продолжать после просмотра скрипта'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2347700990
 | 
			
		||||
value = 'Сборка пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2105058868
 | 
			
		||||
value = 'Скачивание файлов'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1884485082
 | 
			
		||||
value = 'Скачивание источника'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1519177982
 | 
			
		||||
value = 'Ошибка при сборке пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2125220917
 | 
			
		||||
value = 'Выберите, какие пакеты установить'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 812531604
 | 
			
		||||
value = 'Ошибка при запросе выбора пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1040982801
 | 
			
		||||
value = 'Обновление версии'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2235794125
 | 
			
		||||
value = 'Удалить зависимости сборки?'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2205430948
 | 
			
		||||
value = 'Установка зависимостей сборки'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2522710805
 | 
			
		||||
value = 'Установка зависимостей'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3602138206
 | 
			
		||||
value = 'Ошибка при установке пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1057080231
 | 
			
		||||
value = 'Вызов функции package()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2687735200
 | 
			
		||||
value = 'Вызов функции prepare()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 535572372
 | 
			
		||||
value = 'Вызов функции version()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 436644691
 | 
			
		||||
value = 'Вызов функции build()'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2562049386
 | 
			
		||||
value = "Архитектура процессора вашей системы не соответствует этому пакету. Продолжать несмотря на это?"
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3759891273
 | 
			
		||||
value = 'Функция package() необходима'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 4006393493
 | 
			
		||||
value = 'Массив checksums должен быть той же длины, что и sources'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1393316459
 | 
			
		||||
value = 'Этот пакет уже установлен'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1267660189
 | 
			
		||||
value = 'Источник может быть обновлен, если требуется, обновляем'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 21753247
 | 
			
		||||
value = 'Источник найден в кэше'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 257354570
 | 
			
		||||
value = 'Сжатие пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2952487371
 | 
			
		||||
value = 'Создание метаданных пакета'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3121791194
 | 
			
		||||
value = 'Запуск LURE от имени root запрещен, так как это может привести к катастрофическому повреждению вашей системы'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1256604213
 | 
			
		||||
value = 'Ожидание метаданных торрента'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 432261354
 | 
			
		||||
value = 'Скачивание торрент-файла'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1579384326
 | 
			
		||||
value = 'название'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3206337475
 | 
			
		||||
value = 'версия'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1810056261
 | 
			
		||||
value = 'новая'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 1602912115
 | 
			
		||||
value = 'источник'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 2363381545
 | 
			
		||||
value = 'вид'
 | 
			
		||||
 | 
			
		||||
[[translation]]
 | 
			
		||||
id = 3419504365
 | 
			
		||||
value = 'протокол-скачивание'
 | 
			
		||||
							
								
								
									
										56
									
								
								internal/translations/translations.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								internal/translations/translations.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package translations
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"context"
 | 
			
		||||
	"embed"
 | 
			
		||||
	"sync"
 | 
			
		||||
 | 
			
		||||
	"go.elara.ws/logger"
 | 
			
		||||
	"lure.sh/lure/pkg/loggerctx"
 | 
			
		||||
	"go.elara.ws/translate"
 | 
			
		||||
	"golang.org/x/text/language"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
//go:embed files
 | 
			
		||||
var translationFS embed.FS
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	mu         sync.Mutex
 | 
			
		||||
	translator *translate.Translator
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Translator(ctx context.Context) *translate.Translator {
 | 
			
		||||
	mu.Lock()
 | 
			
		||||
	defer mu.Unlock()
 | 
			
		||||
	log := loggerctx.From(ctx)
 | 
			
		||||
	if translator == nil {
 | 
			
		||||
		t, err := translate.NewFromFS(translationFS)
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Fatal("Error creating new translator").Err(err).Send()
 | 
			
		||||
		}
 | 
			
		||||
		translator = &t
 | 
			
		||||
	}
 | 
			
		||||
	return translator
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func NewLogger(ctx context.Context, l logger.Logger, lang language.Tag) *translate.TranslatedLogger {
 | 
			
		||||
	return translate.NewLogger(l, *Translator(ctx), lang)
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										70
									
								
								internal/types/build.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										70
									
								
								internal/types/build.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,70 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
import "lure.sh/lure/pkg/manager"
 | 
			
		||||
 | 
			
		||||
type BuildOpts struct {
 | 
			
		||||
	Script      string
 | 
			
		||||
	Manager     manager.Manager
 | 
			
		||||
	Clean       bool
 | 
			
		||||
	Interactive bool
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BuildVars represents the script variables required
 | 
			
		||||
// to build a package
 | 
			
		||||
type BuildVars struct {
 | 
			
		||||
	Name          string   `sh:"name,required"`
 | 
			
		||||
	Version       string   `sh:"version,required"`
 | 
			
		||||
	Release       int      `sh:"release,required"`
 | 
			
		||||
	Epoch         uint     `sh:"epoch"`
 | 
			
		||||
	Description   string   `sh:"desc"`
 | 
			
		||||
	Homepage      string   `sh:"homepage"`
 | 
			
		||||
	Maintainer    string   `sh:"maintainer"`
 | 
			
		||||
	Architectures []string `sh:"architectures"`
 | 
			
		||||
	Licenses      []string `sh:"license"`
 | 
			
		||||
	Provides      []string `sh:"provides"`
 | 
			
		||||
	Conflicts     []string `sh:"conflicts"`
 | 
			
		||||
	Depends       []string `sh:"deps"`
 | 
			
		||||
	BuildDepends  []string `sh:"build_deps"`
 | 
			
		||||
	OptDepends    []string `sh:"opt_deps"`
 | 
			
		||||
	Replaces      []string `sh:"replaces"`
 | 
			
		||||
	Sources       []string `sh:"sources"`
 | 
			
		||||
	Checksums     []string `sh:"checksums"`
 | 
			
		||||
	Backup        []string `sh:"backup"`
 | 
			
		||||
	Scripts       Scripts  `sh:"scripts"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Scripts struct {
 | 
			
		||||
	PreInstall  string `sh:"preinstall"`
 | 
			
		||||
	PostInstall string `sh:"postinstall"`
 | 
			
		||||
	PreRemove   string `sh:"preremove"`
 | 
			
		||||
	PostRemove  string `sh:"postremove"`
 | 
			
		||||
	PreUpgrade  string `sh:"preupgrade"`
 | 
			
		||||
	PostUpgrade string `sh:"postupgrade"`
 | 
			
		||||
	PreTrans    string `sh:"pretrans"`
 | 
			
		||||
	PostTrans   string `sh:"posttrans"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Directories struct {
 | 
			
		||||
	BaseDir   string
 | 
			
		||||
	SrcDir    string
 | 
			
		||||
	PkgDir    string
 | 
			
		||||
	ScriptDir string
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										38
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								internal/types/config.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
// Config represents the LURE configuration file
 | 
			
		||||
type Config struct {
 | 
			
		||||
	RootCmd          string   `toml:"rootCmd"`
 | 
			
		||||
	PagerStyle       string   `toml:"pagerStyle"`
 | 
			
		||||
	IgnorePkgUpdates []string `toml:"ignorePkgUpdates"`
 | 
			
		||||
	Repos            []Repo   `toml:"repo"`
 | 
			
		||||
	Unsafe           Unsafe   `toml:"unsafe"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Repo represents a LURE repo within a configuration file
 | 
			
		||||
type Repo struct {
 | 
			
		||||
	Name string `toml:"name"`
 | 
			
		||||
	URL  string `toml:"url"`
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type Unsafe struct {
 | 
			
		||||
	AllowRunAsRoot bool `toml:"allowRunAsRoot"`
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								internal/types/repo.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								internal/types/repo.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
/*
 | 
			
		||||
 * LURE - Linux User REpository
 | 
			
		||||
 * Copyright (C) 2023 Elara Musayelyan
 | 
			
		||||
 *
 | 
			
		||||
 * This program is free software: you can redistribute it and/or modify
 | 
			
		||||
 * it under the terms of the GNU General Public License as published by
 | 
			
		||||
 * the Free Software Foundation, either version 3 of the License, or
 | 
			
		||||
 * (at your option) any later version.
 | 
			
		||||
 *
 | 
			
		||||
 * This program is distributed in the hope that it will be useful,
 | 
			
		||||
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 | 
			
		||||
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 | 
			
		||||
 * GNU General Public License for more details.
 | 
			
		||||
 *
 | 
			
		||||
 * You should have received a copy of the GNU General Public License
 | 
			
		||||
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
package types
 | 
			
		||||
 | 
			
		||||
// RepoConfig represents a LURE repo's lure-repo.toml file.
 | 
			
		||||
type RepoConfig struct {
 | 
			
		||||
	Repo struct {
 | 
			
		||||
		MinVersion string `toml:"minVersion"`
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user