1324 lines
46 KiB
Go
1324 lines
46 KiB
Go
package common
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"eon/lib"
|
|
"math"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"plugin"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto/ed25519"
|
|
"database/sql"
|
|
"math/big"
|
|
|
|
"golang.org/x/term"
|
|
|
|
"github.com/Masterminds/semver"
|
|
"github.com/dustin/go-humanize"
|
|
"github.com/fatih/color"
|
|
"modernc.org/sqlite"
|
|
)
|
|
|
|
type InstallPackage struct {
|
|
IsRemote bool
|
|
IsForced bool
|
|
Url string
|
|
Priority int
|
|
StreamOrBytes lib.StreamOrBytes
|
|
EPKPreMap *lib.EPKPreMap
|
|
Repository lib.Repository
|
|
}
|
|
|
|
type PluginInfo struct {
|
|
Name string
|
|
HasErrHandler bool
|
|
HasVagueErr bool
|
|
OverridesExisting bool
|
|
}
|
|
|
|
type Plugin struct {
|
|
Info PluginInfo
|
|
MainFunc func(version string, dbVersion string, args []string, conn *sql.DB, logger *lib.Logger) (etc map[string]interface{}, err error, vagueErr error)
|
|
ErrHandler func(etc map[string]interface{}, err error, vagueErr error, logger *lib.Logger)
|
|
}
|
|
|
|
var conn *sql.DB
|
|
var dbVersion = semver.MustParse("1.0.0-beta.3")
|
|
|
|
var DefaultLogger = lib.Logger{
|
|
LogFunc: func(log lib.Log) string {
|
|
if log.PlaySound {
|
|
fmt.Print("\a")
|
|
}
|
|
switch log.Level {
|
|
case "WARN":
|
|
fmt.Println(color.YellowString(log.Content))
|
|
case "ERROR":
|
|
fmt.Println(color.HiYellowString(log.Content))
|
|
case "CRITICAL":
|
|
fmt.Println(color.HiRedString(log.Content))
|
|
case "FATAL":
|
|
fmt.Println(color.RedString(log.Content))
|
|
os.Exit(1)
|
|
case "PROGRESS":
|
|
// Maths time! We need to calculate the percentage of the progress bar.
|
|
// Convert the total and progress to big.Floats so we can do division.
|
|
floatTotal := new(big.Float)
|
|
floatProgress := new(big.Float)
|
|
floatTotal.SetInt(log.Total)
|
|
floatProgress.SetInt(log.Progress)
|
|
// Calculate the fraction we need to multiply by 100 to get the percentage.
|
|
percentageBig := new(big.Float).Quo(floatProgress, floatTotal)
|
|
percentage, _ := percentageBig.Float64()
|
|
// Get the terminal width so we can calculate the width of the progress bar.
|
|
width, _, err := term.GetSize(int(os.Stdout.Fd()))
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to get terminal width: " + err.Error()))
|
|
os.Exit(1)
|
|
}
|
|
// Calculate the percentage in text form.
|
|
percentageText := " " + strconv.Itoa(int(percentage*100)) + "%"
|
|
// We need to give the percentage text a bit of padding so it doesn't look weird.
|
|
switch len(percentageText) {
|
|
case 3:
|
|
percentageText = " " + percentageText + " "
|
|
case 4:
|
|
percentageText = percentageText + " "
|
|
}
|
|
// Print the percentage text and the beginning of the progress bar.
|
|
fmt.Print("\r" + percentageText + " [")
|
|
|
|
// Calculate how much of the bar should be filled.
|
|
targetWidth := int(float64(width-8) * percentage)
|
|
// Print the filled part of the bar.
|
|
for range targetWidth {
|
|
fmt.Print("=")
|
|
}
|
|
// If it isn't 100%, print the arrow.
|
|
if int(percentage) != 1 {
|
|
fmt.Print(">")
|
|
}
|
|
// Print the empty part of the bar.
|
|
for range width - targetWidth - 9 {
|
|
fmt.Print(" ")
|
|
}
|
|
// And voilà! Cap the bar off. Don't put a space here, because it causes a newline in some terminals.
|
|
if log.Overwrite {
|
|
fmt.Print("]")
|
|
} else {
|
|
fmt.Print("]\n")
|
|
}
|
|
default:
|
|
fmt.Println(log.Content)
|
|
}
|
|
if log.Prompt {
|
|
fmt.Print(": ")
|
|
var userInput string
|
|
_, err := fmt.Scanln(&userInput)
|
|
if err != nil {
|
|
fmt.Println(color.RedString("Failed to read input: " + err.Error()))
|
|
os.Exit(1)
|
|
} else {
|
|
return userInput
|
|
}
|
|
}
|
|
return ""
|
|
},
|
|
PromptSupported: true,
|
|
ProgressSupported: true,
|
|
}
|
|
|
|
var TimeMagnitudes = []humanize.RelTimeMagnitude{
|
|
{0, "now", 1},
|
|
{2 * time.Millisecond, "1 millisecond", 1},
|
|
{time.Second, "%d milliseconds", time.Millisecond},
|
|
// The following code is from an Expat / MIT licensed project.
|
|
// Copyright (c) 2005-2008 Dustin Sallings <dustin@spy.net>
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
// SOFTWARE.
|
|
//
|
|
// <http://www.opensource.org/licenses/mit-license.php>
|
|
|
|
// It is sublicensed under GPLv3.
|
|
{2 * time.Second, "1 second", 1},
|
|
{time.Minute, "%d seconds", time.Second},
|
|
{2 * time.Minute, "1 minute", 1},
|
|
{time.Hour, "%d minutes", time.Minute},
|
|
{2 * time.Hour, "1 hour", 1},
|
|
{humanize.Day, "%d hours", time.Hour},
|
|
{2 * humanize.Day, "1 day", 1},
|
|
{humanize.Week, "%d days", humanize.Day},
|
|
{2 * humanize.Week, "1 week", 1},
|
|
{humanize.Month, "%d weeks", humanize.Week},
|
|
{2 * humanize.Month, "1 month", 1},
|
|
{humanize.Year, "%d months", humanize.Month},
|
|
{18 * humanize.Month, "1 year", 1},
|
|
{2 * humanize.Year, "2 years", 1},
|
|
{humanize.Year, "%d years", humanize.Year},
|
|
{math.MaxInt64, "a long while", 1},
|
|
// End of Expat / MIT licensed code
|
|
}
|
|
|
|
func HandleDependencies(previousDeps int, targetEPK InstallPackage, parentPriority int, epkList []lib.RemoteEPK, ListRemotePackagesInDB func() ([]lib.RemoteEPK, error), InstallPackageList *[]InstallPackage, logger *lib.Logger) (int, error) {
|
|
// Iterate through the dependencies of the target EPK.
|
|
dependencyLoop:
|
|
for _, dependency := range targetEPK.EPKPreMap.Dependencies {
|
|
// Check if the dependency is already in the list of EPKs to install.
|
|
for iterator, epk := range *InstallPackageList {
|
|
if epk.EPKPreMap.Name == dependency {
|
|
// The dependency is already in the list of EPKs to install, check for its dependencies.
|
|
if len(epk.EPKPreMap.Dependencies) == 0 || epk.EPKPreMap.Dependencies == nil {
|
|
// All dependencies are handled - change the priority and continue with the next dependency.
|
|
epk.Priority = parentPriority + 1
|
|
currentInstallPackageList := *InstallPackageList
|
|
currentInstallPackageList[iterator] = epk
|
|
InstallPackageList = ¤tInstallPackageList
|
|
continue dependencyLoop
|
|
} else {
|
|
// Check if it's a circular dependency.
|
|
for _, epk := range *InstallPackageList {
|
|
if epk.EPKPreMap.Name == targetEPK.EPKPreMap.Name {
|
|
// We have a circular dependency. Crash immediately.
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Circular dependency detected: " + targetEPK.EPKPreMap.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.Name,
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
// Recursively handle dependencies.
|
|
addedDeps, err := HandleDependencies(previousDeps, epk, parentPriority+1, epkList, ListRemotePackagesInDB, InstallPackageList, logger)
|
|
if err != nil {
|
|
return 0, err
|
|
} else {
|
|
// Add the dependencies to the total number of dependencies.
|
|
previousDeps += addedDeps
|
|
// All dependencies are now handled - change the priority and continue with the next dependency.
|
|
epk.Priority = parentPriority + 1
|
|
currentInstallPackageList := *InstallPackageList
|
|
currentInstallPackageList[iterator] = epk
|
|
InstallPackageList = ¤tInstallPackageList
|
|
continue dependencyLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check if we already have a list of remote EPKs to use, if so use it.
|
|
if len(epkList) == 0 || epkList == nil {
|
|
var err error
|
|
epkList, err = ListRemotePackagesInDB()
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
}
|
|
|
|
// Check if we already have the EPK installed.
|
|
version, exists, err := DefaultCheckEPKInDB(dependency)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
var remoteEPK lib.RemoteEPK
|
|
var epkEntry InstallPackage
|
|
var dependencyExists bool
|
|
// Iterate through the list of remote EPKs to find the dependency.
|
|
for _, epk := range epkList {
|
|
if epk.Name == dependency {
|
|
dependencyExists = true
|
|
if !exists {
|
|
remoteEPK = epk
|
|
break
|
|
} else {
|
|
// Check if the installed version is outdated.
|
|
if version.LessThan(&epk.Version) {
|
|
// Yes it is: Install the new version as an upgrade.
|
|
remoteEPK = epk
|
|
break
|
|
} else {
|
|
// The installed version is up-to-date.
|
|
continue dependencyLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If the dependency doesn't exist, crash.
|
|
if !dependencyExists {
|
|
return 0, errors.New("dependency " + dependency + " does not exist")
|
|
}
|
|
// Increase the dependency's priority.
|
|
epkEntry.Priority = parentPriority + 1
|
|
// Add the dependency to the list of EPKs to install.
|
|
epkDownloadUrl, err := url.JoinPath(remoteEPK.Repository.URL, remoteEPK.Path)
|
|
if err != nil {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to join URL path: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
// Set the URL and the fact that it is remote.
|
|
epkEntry.IsRemote = true
|
|
epkEntry.Url = epkDownloadUrl
|
|
// Map the EPKs display data.
|
|
epkPreMap, err, vagueErr := lib.PreMapRemoteEPK(remoteEPK, logger)
|
|
if err != nil || vagueErr != nil {
|
|
return 0, err
|
|
}
|
|
// Set the EPKs display data.
|
|
epkEntry.EPKPreMap = &epkPreMap
|
|
if exists {
|
|
// Set it as upgrade if it already exists.
|
|
epkEntry.EPKPreMap.IsUpgrade = true
|
|
}
|
|
// Set the repository.
|
|
epkEntry.Repository = remoteEPK.Repository
|
|
// Make it noted that this is not a forced installation.
|
|
epkEntry.IsForced = false
|
|
// Check if the dependency has dependencies, and if so, recursively handle them.
|
|
if !(len(remoteEPK.Dependencies) == 0 || remoteEPK.Dependencies == nil) {
|
|
// Check if it's a circular dependency.
|
|
for _, epk := range *InstallPackageList {
|
|
if epk.EPKPreMap.Name == targetEPK.EPKPreMap.Name {
|
|
// We have a circular dependency. Crash immediately.
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Circular dependency detected: " + targetEPK.EPKPreMap.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.Name,
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
// Recursively handle dependencies.
|
|
addedDeps, err := HandleDependencies(previousDeps, epkEntry, epkEntry.Priority+1, epkList, ListRemotePackagesInDB, InstallPackageList, logger)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
// Add the dependencies to the total number of dependencies.
|
|
previousDeps += addedDeps
|
|
}
|
|
// All dependencies are now handled - continue with the next dependency.
|
|
currentInstallPackageList := append(*InstallPackageList, epkEntry)
|
|
InstallPackageList = ¤tInstallPackageList
|
|
previousDeps++
|
|
continue
|
|
}
|
|
|
|
// If we reach this point, all dependencies have been handled.
|
|
return previousDeps, nil
|
|
}
|
|
|
|
func GetTotalSize(InstallPackageList []InstallPackage) *big.Int {
|
|
totalSize := new(big.Int)
|
|
for _, epk := range InstallPackageList {
|
|
totalSize.Add(totalSize, big.NewInt(epk.EPKPreMap.Size))
|
|
}
|
|
return totalSize
|
|
}
|
|
|
|
func GetTotalInstalledSize(InstallPackageList []InstallPackage) *big.Int {
|
|
totalSize := new(big.Int)
|
|
for _, epk := range InstallPackageList {
|
|
if !epk.IsRemote {
|
|
totalSize.Add(totalSize, epk.EPKPreMap.DecompressedSize)
|
|
}
|
|
}
|
|
return totalSize
|
|
}
|
|
|
|
func LoadPlugin(pluginName string, logger *lib.Logger) (Plugin, error) {
|
|
// Check the plugin's signature
|
|
// Load the plugin file
|
|
pluginFile, err := os.ReadFile("/var/lib/eon/plugins/" + pluginName + ".so")
|
|
if err != nil {
|
|
return Plugin{}, err
|
|
}
|
|
// Load the signature file
|
|
bypassSignature := false
|
|
sigFile, err := os.ReadFile("/var/lib/eon/plugins/" + pluginName + ".eps")
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
response := logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "Plugin " + pluginName + " is not signed. That means that this code is UNTRUSTED and may be " +
|
|
"MALWARE. This can seriously harm your system! Only proceed if you trust the plugin author, and " +
|
|
"are absolutely 100% sure it is from them! Do you want to load the plugin anyway (y/n)?",
|
|
Prompt: true,
|
|
PlaySound: true,
|
|
})
|
|
if strings.ToLower(response) != "y" {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Plugin " + pluginName + " is not signed. Refusing to load the plugin. Glad you have some " +
|
|
"common sense!",
|
|
Prompt: false,
|
|
})
|
|
} else {
|
|
// This is your final warning before you blow your system to bits.
|
|
// If you are a confused user looking through the source code, seriously, DO NOT load the plugin!
|
|
// Any good plugin author should sign their plugins. Even if you are testing a plugin, you should
|
|
// sign it. If your friend doesn't sign their plugin, tell them to sign it!
|
|
finalWarning := logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "Final warning. Is what you're doing really worth risking your entire system? If someone " +
|
|
"is telling you to bypass this warning, they are probably LYING! Are you SURE you know what " +
|
|
"you're doing (if you do, you probably won't be here)? If you would like to load the plugin, " +
|
|
"please type the following (case sensitive): \"Yes, I know what I'm doing, no one is telling me " +
|
|
"to do this, destroy my system!\". Otherwise, type anything else.",
|
|
Prompt: true,
|
|
PlaySound: true,
|
|
})
|
|
if finalWarning != "Yes, I know what I'm doing, no one is telling me to do this, destroy my system!" {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Plugin " + pluginName + " is not signed. Refusing to load the plugin. Glad you have " +
|
|
"some common sense!",
|
|
Prompt: false,
|
|
})
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "I'll give you 5 seconds to Ctrl+C out of this. If you don't, you're on your own.",
|
|
Prompt: false,
|
|
})
|
|
time.Sleep(5 * time.Second)
|
|
logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "You asked for it. Loading plugin " + pluginName + " without a signature. Don't say I " +
|
|
"didn't warn you.",
|
|
Prompt: false,
|
|
})
|
|
bypassSignature = true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// Get the public key from the database
|
|
if !bypassSignature {
|
|
err = conn.QueryRow("SELECT fingerprint FROM keys WHERE owner = ?", string(sigFile[96:])).Scan(&sigFile)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
fingerprint := lib.ByteToFingerprint(sigFile[:32])
|
|
logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "The public key for the plugin " + pluginName + " is not in the database:\n " + string(sigFile[96:]) + " " + fingerprint + "\n Do you want to add it to the database (y/n)?",
|
|
Prompt: true,
|
|
})
|
|
}
|
|
}
|
|
if ed25519.Verify(sigFile[:32], pluginFile, sigFile[32:96]) {
|
|
return Plugin{}, errors.New("invalid signature")
|
|
}
|
|
}
|
|
// Load the plugin. Unfortunately, there seems to be no way to load a plugin from memory, so we have to use the
|
|
// filesystem.
|
|
loadedPlugin, err := plugin.Open("/var/lib/eon/plugins/" + pluginName + ".so")
|
|
if err != nil {
|
|
return Plugin{}, err
|
|
}
|
|
// Look up the Info function
|
|
infoSymbol, err := loadedPlugin.Lookup("Info")
|
|
if err != nil {
|
|
return Plugin{}, err
|
|
}
|
|
// Assert the Info function to the correct type
|
|
infoFunction, ok := infoSymbol.(func() PluginInfo)
|
|
if !ok {
|
|
return Plugin{}, errors.New("plugin " + pluginName + " does not have an Info function")
|
|
}
|
|
// Get the plugin info
|
|
pluginInfo := infoFunction()
|
|
// Look up the Main function
|
|
mainSymbol, err := loadedPlugin.Lookup("Main")
|
|
if err != nil {
|
|
return Plugin{}, err
|
|
}
|
|
// Assert the Main function to the correct type
|
|
main, ok := mainSymbol.(func(version string, dbVersion string, args []string, conn *sql.DB, logger *lib.Logger) (etc map[string]interface{}, err error, vagueErr error))
|
|
if !ok {
|
|
return Plugin{}, errors.New("plugin " + pluginName + " does not have a Main function")
|
|
}
|
|
// Return the plugin struct
|
|
return Plugin{
|
|
Info: pluginInfo,
|
|
MainFunc: main,
|
|
}, nil
|
|
}
|
|
|
|
func PluginInstalled(pluginName string) bool {
|
|
_, err := os.Stat("/var/lib/eon/plugins/" + pluginName + ".so")
|
|
if err != nil {
|
|
return false
|
|
} else {
|
|
return true
|
|
}
|
|
}
|
|
|
|
func InitDB(conn *sql.DB) error {
|
|
_, err := conn.Exec("DROP TABLE IF EXISTS global;" +
|
|
"DROP TABLE IF EXISTS packages;" +
|
|
"DROP TABLE IF EXISTS keys;" +
|
|
"DROP TABLE IF EXISTS repositories;" +
|
|
"DROP TABLE IF EXISTS remotePackages;")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Huge query moment. This is way too big to read comfortably, but too bad! It breaks the Jetbrains SQL formatter if
|
|
// I break it into multiple lines.
|
|
_, err = conn.Exec("CREATE TABLE packages (name TEXT NOT NULL UNIQUE, description TEXT NOT NULL, longDescription TEXT NOT NULL, version TEXT NOT NULL, author TEXT NOT NULL, license TEXT NOT NULL, architecture TEXT NOT NULL, size INTEGER NOT NULL, dependencies TEXT NOT NULL, removeScript TEXT NOT NULL, hasRemoveScript BOOLEAN NOT NULL, isDependency BOOLEAN NOT NULL DEFAULT false, repository TEXT)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We store the RSA fingerprint of a public key so we can alert if a key is already in the database.
|
|
_, err = conn.Exec("CREATE TABLE keys (fingerprint BLOB NOT NULL, owner TEXT NOT NULL UNIQUE)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = conn.Exec("CREATE TABLE repositories (url TEXT NOT NULL UNIQUE, name TEXT NOT NULL UNIQUE, owner TEXT NOT NULL, description TEXT NOT NULL)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Way to big. Blame IntelliJ.
|
|
_, err = conn.Exec("CREATE TABLE remotePackages (name TEXT NOT NULL UNIQUE, author TEXT NOT NULL, description TEXT NOT NULL, version TEXT NOT NULL, architecture TEXT NOT NULL, size BLOB NOT NULL, dependencies TEXT NOT NULL, path TEXT NOT NULL, arch TEXT NOT NULL, hash TEXT NOT NULL, repository TEXT NOT NULL)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Having exists here makes sure that there is only one global row in the table. This is a bit of a hack,
|
|
// but it's the only way to make sure that the version and endian-ness are always part of the database.
|
|
|
|
// Note that this is the only hack in this program (so far)! I feel somewhat proud, especially since it's less of
|
|
// a hack and more not utilizing the full power of SQL.
|
|
// - Arzumify
|
|
_, err = conn.Exec("CREATE TABLE global (version TEXT NOT NULL, uniquenessCheck BOOLEAN NOT NULL UNIQUE CHECK (uniquenessCheck = true) DEFAULT true)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// We insert global last so that if the program crashes, the database is not marked as initialized.
|
|
_, err = conn.Exec("INSERT INTO global (version) VALUES (?)",
|
|
dbVersion.String())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// EstablishDBConnection establishes a connection to the database. If the database does not exist, it will be created.
|
|
func EstablishDBConnection(logger *lib.Logger) error {
|
|
_, err := os.Stat("/var/lib/eon/packages.db")
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
err := os.MkdirAll("/var/lib/eon", 0755)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
conn, err = sql.Open("sqlite", "/var/lib/eon/packages.db")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var version string
|
|
err = conn.QueryRow("SELECT version FROM global").Scan(&version)
|
|
if err != nil {
|
|
if err.(*sqlite.Error).Code() == 1 || errors.Is(err, sql.ErrNoRows) {
|
|
// Initialize database
|
|
err := InitDB(conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
} else {
|
|
// Check version
|
|
semanticVersion, err := semver.NewVersion(version)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
isCurrentVersion := semanticVersion.Compare(dbVersion)
|
|
if isCurrentVersion < 0 {
|
|
if !PluginInstalled("database-migration") {
|
|
// Warn the user that the database is outdated
|
|
if logger.PromptSupported {
|
|
response := logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "The database is outdated and may not be compatible with the current version of Eon. " +
|
|
"The database migration plugin is not installed. Would you like to reset the database (y/n)?",
|
|
Prompt: true,
|
|
})
|
|
if strings.ToLower(response) == "y" {
|
|
err := InitDB(conn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The database is corrupted and cannot be used. Please reset the database.",
|
|
Prompt: false,
|
|
})
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The database is corrupted and cannot be used. Please reset the database.",
|
|
Prompt: false,
|
|
})
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "INFO",
|
|
Content: "Loading plugin for database migration... All logs will be returned by the plugin until the migration is complete.",
|
|
Prompt: false,
|
|
})
|
|
loadedPlugin, err := LoadPlugin("database-migration", logger)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
etc, err, vagueErr := loadedPlugin.MainFunc(dbVersion.String(), version, nil, conn, logger)
|
|
if err != nil || vagueErr != nil {
|
|
if loadedPlugin.Info.HasErrHandler {
|
|
loadedPlugin.ErrHandler(etc, err, vagueErr, logger)
|
|
return errors.New("plugin " + loadedPlugin.Info.Name + "'s error handler did not stop program execution")
|
|
} else {
|
|
if loadedPlugin.Info.HasVagueErr {
|
|
return errors.New(vagueErr.Error() + ": " + err.Error())
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "INFO",
|
|
Content: "Database migrated (hopefully). Please consult the logs returned by the plugin for more information",
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
} else if isCurrentVersion > 0 {
|
|
// Warn the user that the database is ahead of the current version of Eon
|
|
logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "The database is ahead of the current version of Eon. This may cause compatibility issues",
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultAddEPKToDB(metadata *lib.Metadata, removeScript []byte, dependency bool, hasRemoveScript bool, size int64, repository ...string) error {
|
|
// If it already exists, delete it. This may happen in a force-install scenario.
|
|
_, err := conn.Exec("DELETE FROM packages WHERE name = ?", metadata.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var dependencies string
|
|
if len(metadata.Dependencies) > 0 {
|
|
// This is the world's most basic JSON marshaller :P
|
|
// - Arzumify
|
|
dependencies = "[" + strings.Join(metadata.Dependencies, ", ") + "]"
|
|
} else {
|
|
// Best to make sure that the dependencies are always a JSON array, even if it's empty. Strings.Join might mess
|
|
// it up by adding whitespace or something.
|
|
dependencies = "[]"
|
|
}
|
|
|
|
// Another fat unreadable line so Jetbrains doesn't break the SQL formatter. Too bad!
|
|
// But seriously though why doesn't Jetbrains support this? It's literally their own IDE which tells me to not make
|
|
// super-long lines, but then doesn't support it.
|
|
// And I pay huge amounts of money for this.
|
|
|
|
// Not that I'm complaining or anything.
|
|
// - Arzumify
|
|
if len(repository) > 0 {
|
|
_, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author,
|
|
metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency,
|
|
repository[0])
|
|
} else {
|
|
_, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author,
|
|
metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency)
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Note I never complain a lot in lib/main.go, because that's the one other people will probably use.
|
|
// common/main.go is very tailored towards cmd/main.go, and is probably worth nothing to anything other than CLIs
|
|
// which like to use my style of logging, error handling, database management, progress bars, etc.
|
|
// - Arzumify
|
|
// P.S I sign off comments which have my personal thoughts. I do write other ones, but they're usually more formal
|
|
// and contain less of... me.
|
|
// Ok I promise I'll stop now and actually let the compiler compile.
|
|
|
|
return nil
|
|
}
|
|
|
|
// DefaultCheckEPKInDB checks if an EPK is in the database.
|
|
func DefaultCheckEPKInDB(name string) (*semver.Version, bool, error) {
|
|
var versionString string
|
|
err := conn.QueryRow("SELECT version FROM packages WHERE name = ?", name).Scan(&versionString)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return &semver.Version{}, false, nil
|
|
}
|
|
return &semver.Version{}, false, err
|
|
}
|
|
version, err := semver.NewVersion(versionString)
|
|
if err != nil {
|
|
return &semver.Version{}, true, err
|
|
}
|
|
return version, true, nil
|
|
}
|
|
|
|
func DefaultGetEPKFromDB(name string) (lib.Metadata, string, bool, bool, int64, error) {
|
|
var metadata lib.Metadata
|
|
var removeScript string
|
|
var dependency bool
|
|
var versionString string
|
|
var dependencies string
|
|
var hasRemoveScript bool
|
|
var size int64
|
|
err := conn.QueryRow("SELECT name, description, longDescription, version, author, license, architecture, size, "+
|
|
"dependencies, removeScript, hasRemoveScript, isDependency FROM packages WHERE name = ?", name).Scan(
|
|
&metadata.Name, &metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author,
|
|
&metadata.License, &metadata.Architecture, &size, &dependencies, &removeScript, &hasRemoveScript,
|
|
&dependency)
|
|
if err != nil {
|
|
return lib.Metadata{}, "", false, false, 0, err
|
|
}
|
|
// This is the world's most basic JSON unmarshaller :P
|
|
// - Arzumify
|
|
metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ")
|
|
version, err := semver.NewVersion(versionString)
|
|
if err != nil {
|
|
return lib.Metadata{}, "", false, false, 0, err
|
|
}
|
|
// For some reason NewVersion returns a pointer
|
|
metadata.Version = *version
|
|
return metadata, removeScript, dependency, hasRemoveScript, size, nil
|
|
}
|
|
|
|
func DefaultAddFingerprintToDB(fingerprint []byte, owner string, replace bool) error {
|
|
if replace {
|
|
_, err := conn.Exec("UPDATE keys SET owner = ? AND fingerprint = ? WHERE owner = ? OR fingerprint = ?", owner, fingerprint, owner, fingerprint)
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
return nil
|
|
}
|
|
} else {
|
|
_, err := conn.Exec("INSERT INTO keys (fingerprint, owner) VALUES (?, ?)", fingerprint, owner)
|
|
if err != nil {
|
|
return err
|
|
} else {
|
|
return nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func DefaultGetFingerprintFromDB(fingerprint []byte, author string) (bool, bool, bool, error) {
|
|
var authorCheck string
|
|
var fingerprintCheck []byte
|
|
err := conn.QueryRow("SELECT owner, fingerprint FROM keys WHERE owner = ? OR fingerprint = ?", author, fingerprint).Scan(&authorCheck, &fingerprintCheck)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, false, false, nil
|
|
}
|
|
return false, false, false, err
|
|
} else {
|
|
if authorCheck == author && bytes.Equal(fingerprintCheck, fingerprint) {
|
|
return true, true, true, nil
|
|
} else if authorCheck == author {
|
|
return true, false, true, nil
|
|
} else {
|
|
return true, true, false, nil
|
|
}
|
|
}
|
|
}
|
|
|
|
func DefaultAddRepositoryToDB(repository lib.Repository, forceReplace bool) error {
|
|
if forceReplace {
|
|
// Delete the repository if it already exists. This may happen in a force-install scenario.
|
|
_, err := conn.Exec("DELETE FROM repositories WHERE name = ?", repository.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
_, err := conn.Exec("INSERT INTO repositories (url, name, owner, description) VALUES (?, ?, ?, ?)",
|
|
repository.URL, repository.Name, repository.Owner, repository.Description)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultRemoveRepositoryFromDB(repository string) error {
|
|
_, err := conn.Exec("DELETE FROM repositories WHERE name = ?", repository)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = conn.Exec("DELETE FROM remotePackages WHERE repository = ?", repository)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultCheckRepositoryInDB(repository string) (bool, error) {
|
|
var name string
|
|
err := conn.QueryRow("SELECT name FROM repositories WHERE name = ?", repository).Scan(&name)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return false, nil
|
|
}
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
func DefaultListRepositoriesInDB() ([]lib.Repository, error) {
|
|
rows, err := conn.Query("SELECT url, name, owner, description FROM repositories")
|
|
if err != nil {
|
|
return []lib.Repository{}, err
|
|
}
|
|
var repositories []lib.Repository
|
|
for rows.Next() {
|
|
var repoUrl, name, owner, description string
|
|
err := rows.Scan(&repoUrl, &name, &owner, &description)
|
|
if err != nil {
|
|
return []lib.Repository{}, err
|
|
}
|
|
repositories = append(repositories, lib.Repository{Name: name, URL: repoUrl, Owner: owner, Description: description})
|
|
}
|
|
return repositories, nil
|
|
}
|
|
|
|
func DefaultAddRemotePackageToDB(remoteEPK lib.RemoteEPK) error {
|
|
// Delete the package if it already exists. This may happen in a force-install scenario.
|
|
_, err := conn.Exec("DELETE FROM remotePackages WHERE name = ?", remoteEPK.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var dependencies string
|
|
// This is the world's most basic JSON marshaller :P
|
|
// - Arzumify
|
|
if len(remoteEPK.Dependencies) > 0 {
|
|
dependencies = "[" + strings.Join(remoteEPK.Dependencies, ", ") + "]"
|
|
} else {
|
|
// Best to make sure that the dependencies are always a JSON array, even if it's empty. Strings.Join might mess
|
|
// it up by adding whitespace or something.
|
|
dependencies = "[]"
|
|
}
|
|
|
|
// We use little-endian because big-endianness is never used on 99% of systems. Also, byte order is a myth.
|
|
hashBytes := make([]byte, 8)
|
|
binary.LittleEndian.PutUint64(hashBytes, remoteEPK.EPKHash)
|
|
|
|
_, err = conn.Exec("INSERT INTO remotePackages (name, author, description, version, architecture, size, dependencies, path, arch, hash, repository) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
remoteEPK.Name, remoteEPK.Author, remoteEPK.Description, remoteEPK.Version.String(), remoteEPK.Architecture,
|
|
remoteEPK.CompressedSize, dependencies, remoteEPK.Path, remoteEPK.Arch, hashBytes, remoteEPK.Repository.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func DefaultRemoveRemotePackageFromDB(name string) error {
|
|
_, err := conn.Exec("DELETE FROM remotePackages WHERE name = ?", name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultListRemotePackagesInDB() ([]lib.RemoteEPK, error) {
|
|
rows, err := conn.Query("SELECT name, author, description, version, architecture, size, dependencies, path, arch, hash, repository FROM remotePackages")
|
|
if err != nil {
|
|
return []lib.RemoteEPK{}, err
|
|
}
|
|
var remotePackages []lib.RemoteEPK
|
|
for rows.Next() {
|
|
var name, author, description, version, architecture, dependencies, path, arch, repository string
|
|
var hashBytes []byte
|
|
var size int64
|
|
err := rows.Scan(&name, &author, &description, &version, &architecture, &size, &dependencies, &path, &arch,
|
|
&hashBytes, &repository)
|
|
if err != nil {
|
|
return []lib.RemoteEPK{}, err
|
|
}
|
|
|
|
var repositoryStruct lib.Repository
|
|
repositoryStruct.Name = repository
|
|
err = conn.QueryRow("SELECT url, owner, description FROM repositories WHERE name = ?", repository).Scan(
|
|
&repositoryStruct.URL, &repositoryStruct.Owner, &repositoryStruct.Description)
|
|
if err != nil {
|
|
return []lib.RemoteEPK{}, err
|
|
}
|
|
|
|
// This is the world's most basic JSON unmarshaller :P
|
|
// - Arzumify
|
|
dependenciesList := strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ")
|
|
|
|
// We use little-endian because big-endianness is never used on 99% of systems. Also, byte order is a myth.
|
|
hash := binary.LittleEndian.Uint64(hashBytes)
|
|
|
|
// We use MustParse because we know the version is valid. If it isn't, someone was messing with the database,
|
|
// and that's no longer our problem.
|
|
remotePackages = append(remotePackages, lib.RemoteEPK{
|
|
Name: name,
|
|
Author: author,
|
|
Description: description,
|
|
Version: *semver.MustParse(version),
|
|
Architecture: architecture,
|
|
CompressedSize: size,
|
|
Dependencies: dependenciesList,
|
|
EPKHash: hash,
|
|
Repository: repositoryStruct,
|
|
Path: path,
|
|
Arch: arch,
|
|
})
|
|
}
|
|
|
|
return remotePackages, nil
|
|
}
|
|
|
|
func PrintWithEvenPadding(item string, maxSize int) {
|
|
var printItem string
|
|
// Make sure green escape sequences are not counted in the length by using a regex.
|
|
noGreenEscape := regexp.MustCompile(`\x1b\[32m|\x1b\[0m`)
|
|
// Now we can count the length of the string without the green escape sequences.
|
|
itemLength := len(noGreenEscape.ReplaceAllString(item, ""))
|
|
// Let's calculate how much padding we need to fit itemLength into maxSize.
|
|
padding := maxSize - itemLength
|
|
// The padding will always be positive due to an earlier check.
|
|
// Check if the padding is even or odd.
|
|
if padding%2 == 0 {
|
|
// If it's even, we can just divide it by 2 and add that amount of padding to the left and right.
|
|
leftPadding := strings.Repeat(" ", padding/2)
|
|
rightPadding := strings.Repeat(" ", padding/2)
|
|
printItem = leftPadding + item + rightPadding
|
|
} else {
|
|
// If it's odd, we add one more space to the right padding.
|
|
leftPadding := strings.Repeat(" ", padding/2)
|
|
rightPadding := strings.Repeat(" ", padding/2+1)
|
|
printItem = leftPadding + item + rightPadding
|
|
}
|
|
// Print the string.
|
|
fmt.Print(printItem)
|
|
}
|
|
|
|
func RefreshPackageList(listRepositoriesInDB func() ([]lib.Repository, error), addRemotePackageToDB func(lib.RemoteEPK) error, getFingerprintFromDB func([]byte, string) (bool, bool, bool, error), addFingerprintToDB func([]byte, string, bool) error, checkRepositoryInDB func(string) (bool, error), addRepositoryToDB func(lib.Repository, bool) error, listRemotePackagesInDB func() ([]lib.RemoteEPK, error), removeRemotePackageFromDB func(string) error, logger *lib.Logger) error {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "INFO",
|
|
Content: "Refreshing package list...",
|
|
})
|
|
|
|
startTime := time.Now()
|
|
|
|
// Fetch new packages from repositories, that's easy.
|
|
repositories, err := listRepositoriesInDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
repositoryMap := make(map[string]lib.Repository)
|
|
for _, repository := range repositories {
|
|
repositoryMap[repository.Name] = repository
|
|
_, err, vagueErr := lib.AddRepository(repository.URL, addRepositoryToDB, getFingerprintFromDB, addFingerprintToDB,
|
|
addRemotePackageToDB, checkRepositoryInDB, true, logger)
|
|
if err != nil || vagueErr != nil {
|
|
AddRepositoryHandleError(err, vagueErr, logger)
|
|
}
|
|
}
|
|
|
|
// We need to check if there are any orphaned packages with URLs that no longer exist.
|
|
packages, err := listRemotePackagesInDB()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
removedOrphanedPackages := 0
|
|
for _, remoteEpk := range packages {
|
|
testUrl, err := url.JoinPath(remoteEpk.Repository.URL, remoteEpk.Path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
response, err := http.Head(testUrl)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if response.StatusCode != 200 && response.StatusCode != 404 {
|
|
// We don't delete unless it's 404 because the server may be down.
|
|
logger.LogFunc(lib.Log{
|
|
Level: "WARN",
|
|
Content: "The package " + remoteEpk.Name + " has returned a status code of " + strconv.Itoa(response.StatusCode) + ". This may indicate a problem with the repository.",
|
|
Prompt: false,
|
|
})
|
|
} else if response.StatusCode == 404 {
|
|
// Delete the package.
|
|
err := removeRemotePackageFromDB(remoteEpk.Name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
removedOrphanedPackages++
|
|
}
|
|
}
|
|
|
|
logger.LogFunc(lib.Log{
|
|
Level: "INFO",
|
|
Content: "Refreshed package list in " + humanize.CustomRelTime(startTime, time.Now(), "", "",
|
|
TimeMagnitudes) + ". Removed " + strconv.Itoa(removedOrphanedPackages) + " orphaned packages.",
|
|
Prompt: false,
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
// I hate the vagueError system - it feels too much like a hack, but there's no chance in hell I'm going to find every
|
|
// error from every library I use and make a HandleError function for it. I'm not that insane.
|
|
// Anyway I should put a useful formal comment so that people using IntelliSense or whatever it's called in VSCode can
|
|
// see what this function does.
|
|
// - Arzumify
|
|
|
|
// PreMapEPKHandleError handles errors that occur during the mapping of an EPKs display data.
|
|
func PreMapEPKHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKFailedToReadError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to read file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKNotEPKError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is not an EPK.",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKInvalidEndianError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is corrupted or invalid: invalid endian",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotParseJSONError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to parse metadata JSON: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSONError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to map metadata JSON: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// FullyMapMetadataHandleError handles errors that occur during the mapping of an EPK file.
|
|
func FullyMapMetadataHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToReadError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to read file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToJumpError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to jump to metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToAddFingerprintError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to add fingerprint to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to get fingerprint from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataInvalidSignatureError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is corrupted or invalid: signature mismatch",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSONError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to map metadata JSON: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// InstallEPKHandleError handles errors that occur during the installation of an EPK file.
|
|
func InstallEPKHandleError(tempDir string, err error, vagueErr error, logger *lib.Logger) {
|
|
doNotRemoveTempDir := false
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateTempDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create temporary directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReaderError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create ZStandard reader: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchiveError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to decompress tar archive: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "ERROR",
|
|
Content: "Could not get file information about directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFileError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "ERROR",
|
|
Content: "Could not get file information about file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFileError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReaderError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to close tar reader: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHookError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get file information about hook: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHookError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to run hook: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDBError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to add EPK to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "CRITICAL",
|
|
Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually",
|
|
Prompt: false,
|
|
})
|
|
doNotRemoveTempDir = true
|
|
}
|
|
if doNotRemoveTempDir {
|
|
err := os.RemoveAll(tempDir)
|
|
if err != nil {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "CRITICAL",
|
|
Content: "Failed to remove temporary directory: " + err.Error() + ", please remove the directory " + tempDir + " manually",
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func AddRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateRequestError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotSendRequestError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to send request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryUnexpectedStatusCodeError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Unexpected status code: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotReadResponseError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to read response: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMagicError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Invalid magic: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotHashError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to copy file to hash: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryUnmarshalMetadataError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to unmarshal metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataFailedToGetFingerprintError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to get fingerprint from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddFingerprintError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to add fingerprint to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryInvalidMetadataError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Invalid metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotCreateDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to create repository directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCannotStatDirError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to get file information about repository directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddPackageError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to add package to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryRepositoryAlreadyExistsError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Repository already exists",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryFailedToAddRepositoryError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to add repository to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func RemoveRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExistError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Repository does not exist",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotFindRepositoryError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to get file information about repository: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCannotRemoveRepositoryError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to remove repository files: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryFailedToRemoveRepositoryFromDBError):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Failed to remove repository from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Ironic how I sign off comments with my name when I'm literally the only contributor to this project, and git shows
|
|
// my name anyway. What is this, ftp?
|
|
// - Arzumify
|