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