1530 lines
53 KiB
Go
1530 lines
53 KiB
Go
package common
|
|
|
|
import (
|
|
"eon/lib"
|
|
|
|
"bufio"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"plugin"
|
|
"regexp"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"crypto/ed25519"
|
|
"database/sql"
|
|
"encoding/binary"
|
|
"net/http"
|
|
"net/url"
|
|
|
|
"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 RemovePackage struct {
|
|
Name string
|
|
Priority int
|
|
DisplayData lib.DisplayData
|
|
}
|
|
|
|
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
|
|
|
|
// Database is no longer compatible with versions below 3.0.0.
|
|
var dbVersion = semver.MustParse("3.0.0")
|
|
|
|
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 floats so we can do division.
|
|
var floatTotal = float64(log.Total)
|
|
var floatProgress = float64(log.Progress)
|
|
// Calculate the percentage.
|
|
var percentage = floatProgress / floatTotal
|
|
// 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("Could not 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(": ")
|
|
reader := bufio.NewReader(os.Stdin)
|
|
userInput, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
fmt.Println(color.RedString("[FATAL]"), "Could not read user input:", err)
|
|
os.Exit(1)
|
|
} else {
|
|
return userInput[:len(userInput)-1]
|
|
}
|
|
}
|
|
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), listEPKsInDB func() (map[string]lib.DisplayData, map[string]uint64, map[string]lib.Repository, map[string]uint64, error), InstallPackageList []InstallPackage, previousPackages *[]string, logger *lib.Logger) ([]InstallPackage, int, error) {
|
|
// Iterate through the dependencies of the target EPK.
|
|
dependencyLoop:
|
|
for _, dependency := range targetEPK.EPKPreMap.DisplayData.Dependencies {
|
|
// Check if the dependency is already in the list of EPKs to install.
|
|
for iterator, epk := range InstallPackageList {
|
|
if epk.EPKPreMap.DisplayData.Name == dependency {
|
|
// The dependency is already in the list of EPKs to install, check for its dependencies.
|
|
if len(epk.EPKPreMap.DisplayData.Dependencies) == 0 || epk.EPKPreMap.DisplayData.Dependencies == nil {
|
|
// All dependencies are handled - change the priority and continue with the next dependency.
|
|
epk.Priority = parentPriority + 1
|
|
InstallPackageList[iterator] = epk
|
|
continue dependencyLoop
|
|
} else {
|
|
// Check if it's a circular dependency.
|
|
for _, epk := range *previousPackages {
|
|
if epk == targetEPK.EPKPreMap.DisplayData.Name {
|
|
// We have a circular dependency. Crash immediately.
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name,
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
// Add the dependency to the list of previous packages.
|
|
*previousPackages = append(*previousPackages, targetEPK.EPKPreMap.DisplayData.Name)
|
|
// Recursively handle dependencies.
|
|
installedPackage, addedDeps, err := HandleDependencies(previousDeps, epk, parentPriority+1, epkList, listRemotePackagesInDB, listEPKsInDB, InstallPackageList, previousPackages, logger)
|
|
InstallPackageList = installedPackage
|
|
if err != nil {
|
|
return nil, 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
|
|
InstallPackageList[iterator] = epk
|
|
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 nil, 0, err
|
|
}
|
|
}
|
|
|
|
// Check if we already have the EPK installed.
|
|
version, exists, err := DefaultCheckEPKInDB(dependency)
|
|
if err != nil {
|
|
return nil, 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: add it to the list of EPKs to install.
|
|
remoteEPK = epk
|
|
break
|
|
} else {
|
|
// The installed version is up-to-date.
|
|
continue dependencyLoop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if !dependencyExists {
|
|
// Iterate through the list of installed EPKs to find the dependency.
|
|
installedEPKs, _, _, _, err := listEPKsInDB()
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
for installedEPK := range installedEPKs {
|
|
if installedEPK == dependency {
|
|
// The dependency is installed. Exit the loop.
|
|
continue dependencyLoop
|
|
}
|
|
}
|
|
|
|
// If the dependency still doesn't exist, crash.
|
|
if !dependencyExists {
|
|
return nil, 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: "Could not 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 nil, 0, err
|
|
}
|
|
epkPreMap.DisplayData.IsDependency = true
|
|
// 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 *previousPackages {
|
|
if epk == targetEPK.EPKPreMap.DisplayData.Name {
|
|
// We have a circular dependency. Crash immediately.
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Circular dependency detected: " + targetEPK.EPKPreMap.DisplayData.Name + " -> " + dependency + " -> " + targetEPK.EPKPreMap.DisplayData.Name,
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
// Add the dependency to the list of previous packages.
|
|
*previousPackages = append(*previousPackages, epkEntry.EPKPreMap.DisplayData.Name)
|
|
// Recursively handle dependencies.
|
|
installPackages, addedDeps, err := HandleDependencies(previousDeps, epkEntry, epkEntry.Priority+1, epkList, listRemotePackagesInDB, listEPKsInDB, InstallPackageList, previousPackages, logger)
|
|
InstallPackageList = installPackages
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
|
|
// Add the dependencies to the total number of dependencies.
|
|
previousDeps += addedDeps
|
|
}
|
|
// All dependencies are now handled - continue with the next dependency.
|
|
InstallPackageList = append(InstallPackageList, epkEntry)
|
|
previousDeps++
|
|
continue
|
|
}
|
|
|
|
// If we reach this point, all dependencies have been handled.
|
|
return InstallPackageList, previousDeps, nil
|
|
}
|
|
|
|
func GetTotalSize(InstallPackageList []InstallPackage) uint64 {
|
|
var totalSize uint64
|
|
for _, epk := range InstallPackageList {
|
|
totalSize += epk.EPKPreMap.DisplayData.Size
|
|
}
|
|
return totalSize
|
|
}
|
|
|
|
func GetTotalInstalledSize(InstallPackageList []InstallPackage) uint64 {
|
|
var totalSize uint64
|
|
for _, epk := range InstallPackageList {
|
|
if !epk.IsRemote {
|
|
totalSize += epk.EPKPreMap.DisplayData.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, installedPaths TEXT, decompressedSize INTEGER NOT NULL)")
|
|
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, ignore BOOLEAN NOT NULL DEFAULT false)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, err = conn.Exec("INSERT INTO repositories (url, name, owner, description, ignore) VALUES ('None', 'Local file', 'eon', 'This is a placeholder repository for local files to be registered to. Do not attempt to use this repository.', true)")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
_, 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
|
|
}
|
|
|
|
_, 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 could not be used. Please reset the database.",
|
|
Prompt: false,
|
|
})
|
|
os.Exit(1)
|
|
}
|
|
} else {
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The database is corrupted and could not 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, installedPaths []string, removeScript []byte, dependency bool, hasRemoveScript bool, size uint64, decompressedSize uint64, 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 = "[]"
|
|
}
|
|
|
|
var installedPathsString string
|
|
if len(installedPaths) > 0 {
|
|
installedPathsString = "[" + strings.Join(installedPaths, ", ") + "]"
|
|
} else {
|
|
installedPathsString = "[]"
|
|
}
|
|
|
|
// 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
|
|
_, err = conn.Exec("INSERT INTO packages (name, description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths, decompressedSize) VALUES (?,?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
|
metadata.Name, metadata.Description, metadata.LongDescription, metadata.Version.String(), metadata.Author,
|
|
metadata.License, metadata.Architecture, size, dependencies, string(removeScript), hasRemoveScript, dependency,
|
|
repository, installedPathsString, decompressedSize)
|
|
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, string, []string, error) {
|
|
var metadata lib.Metadata
|
|
var removeScript string
|
|
var dependency bool
|
|
var versionString string
|
|
var dependencies string
|
|
var hasRemoveScript bool
|
|
var repository string
|
|
var installedPaths string
|
|
var size int64
|
|
err := conn.QueryRow("SELECT description, longDescription, version, author, license, architecture, size, dependencies, removeScript, hasRemoveScript, isDependency, repository, installedPaths, decompressedSize FROM packages WHERE name = ?",
|
|
name).Scan(&metadata.Description, &metadata.LongDescription, &versionString, &metadata.Author,
|
|
&metadata.License, &metadata.Architecture, &size, &dependencies, &removeScript, &hasRemoveScript,
|
|
&dependency, &repository, &installedPaths, &metadata.DecompressedSize)
|
|
if err != nil {
|
|
return lib.Metadata{}, "", false, false, 0, "", nil, 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, "", nil, err
|
|
}
|
|
// Also unmarshal the installed paths
|
|
var installedPathsSlice []string
|
|
installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ")
|
|
|
|
// For some reason NewVersion returns a pointer
|
|
metadata.Version = *version
|
|
return metadata, removeScript, dependency, hasRemoveScript, size, repository, installedPathsSlice, nil
|
|
}
|
|
|
|
func DefaultRemoveEPKFromDB(name string) error {
|
|
_, err := conn.Exec("DELETE FROM packages WHERE name = ?", name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func DefaultListEPKsInDB() (map[string]lib.DisplayData, map[string]uint64, map[string]lib.Repository, map[string]uint64, error) {
|
|
rows, err := conn.Query("SELECT name, description, version, author, architecture, size, dependencies, repository, isDependency, decompressedSize FROM packages")
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
|
|
repositoryMap := make(map[string]lib.Repository)
|
|
repoNameToRepo := make(map[string]lib.Repository)
|
|
metadataMap := make(map[string]lib.DisplayData)
|
|
sizes := make(map[string]uint64)
|
|
compressedSizes := make(map[string]uint64)
|
|
for rows.Next() {
|
|
var metadata lib.DisplayData
|
|
var size uint64
|
|
var dependencies string
|
|
var repository string
|
|
var version string
|
|
var isDependency bool
|
|
err := rows.Scan(&metadata.Name, &metadata.Description, &version, &metadata.Author, &metadata.Architecture,
|
|
&size, &dependencies, &repository, &isDependency, &metadata.DecompressedSize)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
metadata.IsDependency = isDependency
|
|
semVer, err := semver.NewVersion(version)
|
|
if err != nil {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
// Stupid pointer
|
|
metadata.Version = *semVer
|
|
metadata.Dependencies = strings.Split(strings.TrimSuffix(strings.TrimPrefix(dependencies, "["), "]"), ", ")
|
|
metadataMap[metadata.Name] = metadata
|
|
sizes[metadata.Name] = size
|
|
compressedSizes[metadata.Name] = size
|
|
// Check if the repository is in the repoNameToRepo map
|
|
var repo lib.Repository
|
|
repo, ok := repoNameToRepo[repository]
|
|
if !ok {
|
|
// If it's not, find the repository and add it to the map
|
|
repo.Name = repository
|
|
err := conn.QueryRow("SELECT url, owner, description FROM repositories WHERE name = ?", repository).Scan(&repo.URL, &repo.Owner, &repo.Description)
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, nil, nil, nil, errors.New("repository " + repository + " not found")
|
|
} else {
|
|
return nil, nil, nil, nil, err
|
|
}
|
|
}
|
|
repoNameToRepo[repository] = repo
|
|
}
|
|
// Add the repository to the repository map
|
|
repositoryMap[metadata.Name] = repo
|
|
}
|
|
|
|
return metadataMap, sizes, repositoryMap, compressedSizes, nil
|
|
}
|
|
|
|
func DefaultGetEPKRemoveInfoFromDB(name string) (string, []string, error) {
|
|
var removeScript string
|
|
var installedPaths string
|
|
err := conn.QueryRow("SELECT removeScript, installedPaths FROM packages WHERE name = ?", name).Scan(&removeScript, &installedPaths)
|
|
if err != nil {
|
|
return "", nil, err
|
|
}
|
|
|
|
// Unmarshal the installed paths
|
|
var installedPathsSlice []string
|
|
installedPathsSlice = strings.Split(strings.TrimSuffix(strings.TrimPrefix(installedPaths, "["), "]"), ", ")
|
|
|
|
return removeScript, installedPathsSlice, 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 = ? OR url = ?", repository.Name, repository.URL)
|
|
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 WHERE ignore = false")
|
|
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 ORDER BY version")
|
|
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 uint64
|
|
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,
|
|
})
|
|
}
|
|
|
|
// Re-sort remotePackages by version, latest first - this stops the package manager from installing older versions.
|
|
sort.Slice(remotePackages, func(i, j int) bool {
|
|
return remotePackages[i].Version.GreaterThan(&remotePackages[j].Version)
|
|
})
|
|
|
|
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.ErrPreMapEPKCouldNotRead):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not read file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKHasNetworkStream):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Network streams are not supported: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is not an EPK.",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKHasInvalidEndian):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is corrupted or invalid: invalid endian",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKCouldNotMapJSON):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not map metadata JSON: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// PreMapRemoteEPKHandleError handles errors that occur during the mapping of a remote EPKs display data.
|
|
func PreMapRemoteEPKHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateURL):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create URL: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCreateRequest):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotSendRequest):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not send request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotRead):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not read EPK: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotCloseConnection):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not close connection: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKUnexpectedStatusCode):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Unexpected status code: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapEPKHasNotGotEPKMagic):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is not an EPK.",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKInvalidEndian):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is corrupted or invalid: invalid endian",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrPreMapRemoteEPKCouldNotMapJSON):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not 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.ErrFullyMapMetadataCouldNotRead):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not read file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotJump):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not jump to metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotAddFingerprint):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not add fingerprint to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get fingerprint from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataHasInvalidSignature):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "The specified file is corrupted or invalid: signature mismatch",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotMapJSON):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not 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.ErrInstallEPKCouldNotCreateTempDir):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create temporary directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateZStandardReader):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create ZStandard reader: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotDecompressTarArchive):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not decompress tar archive: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateDir):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatDir):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "ERROR",
|
|
Content: "Could not get file information about directory: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatFile):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "ERROR",
|
|
Content: "Could not get file information about file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCreateFile):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotCloseTarReader):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not close tar reader: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotStatHook):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get file information about hook: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRunHook):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not run hook: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotAddEPKToDB):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not add EPK to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrInstallEPKCouldNotRemoveTempDir):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "CRITICAL",
|
|
Content: "Could not 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: "Could not 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.ErrAddRepositoryCouldNotCreateRequest):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotSendRequest):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not send request: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryHasUnexpectedStatusCode):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Unexpected status code: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotReadResponse):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not read response: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMagic):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Invalid magic: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotHash):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not copy file to hash: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotUnmarshalMetadata):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not unmarshal metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrFullyMapMetadataCouldNotGetFingerprint):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get fingerprint from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddFingerprint):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not add fingerprint to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryHasInvalidMetadata):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Invalid metadata: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddPackage):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not add package to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryHasRepositoryExists):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Repository already exists",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrAddRepositoryCouldNotAddRepository):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not add repository to database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
|
|
}
|
|
}
|
|
|
|
func RemoveRepositoryHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryDoesNotExist):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Repository does not exist",
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotFindRepository):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get file information about repository: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepository):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not remove repository files: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveRepositoryCouldNotRemoveRepositoryFromDB):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not remove repository from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
}
|
|
}
|
|
|
|
// RemoveEPKHandleError handles errors that occur during the removal of an EPK.
|
|
func RemoveEPKHandleError(err error, vagueErr error, logger *lib.Logger) {
|
|
switch {
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotFindEPK):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not get EPK from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotCreateTempFile):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not create temporary file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotWriteTempFile):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not write to temporary file: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRunRemoveHook):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not run remove hook: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveEPKFromDB):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not remove EPK from database: " + err.Error(),
|
|
Prompt: false,
|
|
})
|
|
case errors.Is(vagueErr, lib.ErrRemoveEPKCouldNotRemoveFiles):
|
|
logger.LogFunc(lib.Log{
|
|
Level: "FATAL",
|
|
Content: "Could not remove files: " + 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
|