eon/cmd/main.go

392 lines
12 KiB
Go
Raw Normal View History

2024-09-01 12:27:25 -07:00
package main
import (
"eon/common"
"eon/lib"
"bufio"
"bytes"
"fmt"
"io"
"os"
"regexp"
"strings"
"golang.org/x/term"
"github.com/dustin/go-humanize"
"github.com/fatih/color"
)
var logger = &common.DefaultLogger
func main() {
if len(os.Args) < 2 {
fmt.Println("Usage: eon <list/info/install/remove/clean/repo/help> [args]")
os.Exit(1)
}
switch os.Args[1] {
case "help":
if len(os.Args) < 3 {
fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]")
os.Exit(1)
} else {
switch os.Args[2] {
case "list":
fmt.Println("Usage: eon list (remote/local)")
fmt.Println("- remote: lists packages available in the repositories.")
fmt.Println("- local: lists installed packages.")
case "info":
fmt.Println("Usage: eon info <package>")
fmt.Println("Shows information about a package.")
case "install":
fmt.Println("Usage: eon install <package> [<repository>]")
fmt.Println("Installs a package.")
case "remove":
fmt.Println("Usage: eon remove <package>")
fmt.Println("Removes a package.")
case "clean":
fmt.Println("Usage: eon clean")
fmt.Println("Removes unused dependencies.")
case "repo":
fmt.Println("Usage: eon repo (add/remove/list)")
fmt.Println("- add <url>: adds a repository.")
fmt.Println("- remove <url>: removes a repository.")
fmt.Println("- list: lists repositories.")
case "help":
fmt.Println("Usage: eon help (list/info/install/remove/clean/repo/help) [args]")
fmt.Println("Shows help.")
default:
fmt.Println(color.RedString("Unknown command: " + os.Args[2]))
os.Exit(1)
}
}
case "install":
var forceMode bool
var inMemoryMode bool
if len(os.Args) < 3 {
fmt.Println("Usage: eon install <package>")
os.Exit(1)
}
var localPackageList []string
fileInfos := make(map[string]os.FileInfo)
var repoPackageList []string
for _, pkg := range os.Args[2:] {
if !strings.HasPrefix(pkg, "-") {
fileInfo, err := os.Stat(pkg)
if err != nil {
if os.IsNotExist(err) {
// This unholy regex is a package name validator. It validates repository-name/package-name/1.0.0-prerelease.1+meta.1
// The regex is so unholy it breaks JetBrains' regex parser, so I had to disable it.
//goland:noinspection RegExpUnnecessaryNonCapturingGroup
match, err := regexp.Match(`^(?:([a-z0-9_-]+)/)?([a-z0-9_-]+)(?:/(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?)$`, []byte(pkg))
if err != nil {
fmt.Println(color.RedString("Failed to validate package name: " + err.Error()))
os.Exit(1)
}
if !match {
fmt.Println(color.RedString("Invalid package name: " + pkg))
os.Exit(1)
} else {
repoPackageList = append(repoPackageList, pkg)
}
}
} else {
localPackageList = append(localPackageList, pkg)
fileInfos[pkg] = fileInfo
}
} else {
switch pkg {
case "--force", "-f":
forceMode = true
case "--optimizeForSpeed", "-O":
inMemoryMode = true
}
}
}
// We contact the database before we print anything as to not break the formatting.
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
// Ok. Let's give the user an overview of what we're going to do.
if len(localPackageList) > 0 || len(repoPackageList) > 0 {
fmt.Println("The following packages will be installed:")
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)
}
if width < 42 {
fmt.Println(color.RedString("Terminal too small. Minimum required width: 42 characters."))
os.Exit(1)
}
for range width {
fmt.Print("=")
}
fmt.Print("\n ")
tableList := []string{"Package", "Architecture", "Version", "Repository", "Size", "Action"}
maxSize := (width - 6) / 6
for _, item := range tableList {
common.PrintWithEvenPadding(item, maxSize)
}
fmt.Print("\n")
for range width {
fmt.Print("=")
}
fmt.Print("\n")
var epkListLocal []common.InstallPackage
for _, pkg := range localPackageList {
var finalisedList []string
epkFile, err := os.Open(pkg)
if err != nil {
fmt.Println(color.RedString("Failed to open package: " + err.Error()))
os.Exit(1)
}
var displayData lib.EPKPreMap
var vagueErr error
var epkBytes bytes.Buffer
if !inMemoryMode {
displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{Stream: epkFile, IsStream: true}, fileInfos[pkg].Size())
} else {
_, err = io.Copy(bufio.NewWriter(&epkBytes), epkFile)
if err != nil {
fmt.Println(color.RedString("Failed to read package into memory: " + err.Error() +
", have you tried disabling in-memory mode?"))
os.Exit(1)
}
err := epkFile.Close()
if err != nil {
fmt.Println(color.HiYellowString("Failed to close package file: " + err.Error() + ", memory leak possible."))
}
displayData, err, vagueErr = lib.PreMapEPK(lib.StreamOrBytes{Bytes: epkBytes.Bytes(), IsStream: false}, fileInfos[pkg].Size())
}
if err != nil || vagueErr != nil {
common.PreMapEPKHandleError(err, vagueErr, logger)
os.Exit(1)
}
version, exists, err := common.DefaultCheckEPKInDB(displayData.Name)
if err != nil {
fmt.Println(color.RedString("Failed to check for package in database: " + err.Error()))
}
// TODO: Implement some dependency management more-or-less here.
name := displayData.Name
architecture := displayData.Architecture
versionDisplay := displayData.Version.String()
repository := "Local file"
size := humanize.Bytes(uint64(displayData.Size))
action := "Install"
if exists {
if version.Compare(&displayData.Version) != -1 {
if !forceMode {
name = color.GreenString(name)
architecture = color.GreenString(architecture)
versionDisplay = color.GreenString(versionDisplay)
repository = color.GreenString(repository)
size = color.GreenString(size)
action = color.GreenString("Skip")
} else {
action = "Forced installation"
displayData.IsUpgrade = true
if !inMemoryMode {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Stream: epkFile,
IsStream: true,
},
EPKPreMap: &displayData,
LocalFile: true,
})
} else {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Bytes: epkBytes.Bytes(),
IsStream: false,
},
EPKPreMap: &displayData,
LocalFile: true,
})
}
}
} else {
action = "Upgrade"
displayData.IsUpgrade = true
if !inMemoryMode {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Stream: epkFile,
IsStream: true,
},
EPKPreMap: &displayData,
LocalFile: true,
})
} else {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Bytes: epkBytes.Bytes(),
IsStream: false,
},
EPKPreMap: &displayData,
LocalFile: true,
})
}
}
} else {
displayData.IsUpgrade = false
if !inMemoryMode {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Stream: epkFile,
IsStream: true,
},
EPKPreMap: &displayData,
LocalFile: true,
})
} else {
epkListLocal = append(epkListLocal, common.InstallPackage{
StreamOrBytes: lib.StreamOrBytes{
Bytes: epkBytes.Bytes(),
IsStream: false,
},
EPKPreMap: &displayData,
LocalFile: true,
})
}
}
finalisedList = append(finalisedList, name)
finalisedList = append(finalisedList, architecture)
finalisedList = append(finalisedList, versionDisplay)
finalisedList = append(finalisedList, repository)
finalisedList = append(finalisedList, size)
finalisedList = append(finalisedList, action)
fmt.Print(" ")
for _, item := range finalisedList {
common.PrintWithEvenPadding(item, maxSize)
}
fmt.Print("\n")
}
//for _, pkg := range repoPackageList {
// var finalisedList []string
// lib.SearchRepository()
//}
for range width {
fmt.Print("=")
}
fmt.Print("\n")
if len(epkListLocal) > 0 {
response := logger.LogFunc(lib.Log{
Level: "INFO",
Content: "Proceed with installation (y/n)?",
Prompt: true,
PlaySound: true,
})
if strings.ToLower(response) == "y" {
// We hide the cursor because it makes the progress bar look weird and other package managers hide
// it during installation. For some reason, it builds suspense. Or it does with me anyway, when
// a program hides the cursor, it makes me think twice than to Ctrl+C it :P
// - Arzumify
fmt.Print("\033[?25l")
// Time to install things.
for _, streamOrBytesAndPreMap := range epkListLocal {
metadata, err, vagueErr := lib.FullyMapMetadata(streamOrBytesAndPreMap.StreamOrBytes,
streamOrBytesAndPreMap.EPKPreMap, common.DefaultGetFingerprintFromDB,
common.DefaultAddFingerprintToDB, logger)
if err != nil || vagueErr != nil {
common.FullyMapMetadataHandleError(err, vagueErr, logger)
}
// Install the package.
tempDir, err, vagueErr := lib.InstallEPK(streamOrBytesAndPreMap.StreamOrBytes,
metadata, streamOrBytesAndPreMap.EPKPreMap, common.DefaultAddEPKToDB, logger)
if err != nil || vagueErr != nil {
common.InstallEPKHandleError(tempDir, err, vagueErr, logger)
}
// Done!
fmt.Println("Installed package: " + streamOrBytesAndPreMap.EPKPreMap.Name)
}
// We show the cursor again because we're done with the progress bar.
// We show the cursor again because we're done with the progress bar.
fmt.Print("\033[?25h")
} else {
fmt.Println("Installation cancelled.")
os.Exit(1)
}
} else {
fmt.Println("No packages left to install. To force re-install / downgrade, use the --force flag, " +
"though this may result in breakage.")
os.Exit(0)
}
} else {
fmt.Println("No packages to install.")
os.Exit(0)
}
case "repo":
if len(os.Args) < 3 {
fmt.Println("Usage: eon repo (add/remove/list)")
os.Exit(1)
}
switch os.Args[2] {
case "add":
if len(os.Args) < 4 {
fmt.Println("Usage: eon repo add <url>")
os.Exit(1)
} else {
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
err, vagueErr := lib.AddRepository(os.Args[3], common.DefaultAddRepositoryToDB, common.DefaultGetFingerprintFromDB, common.DefaultAddFingerprintToDB, common.DefaultAddRemotePackageToDB, logger)
if err != nil || vagueErr != nil {
common.AddRepositoryHandleError(err, vagueErr, logger)
}
}
case "remove":
if len(os.Args) < 4 {
fmt.Println("Usage: eon repo remove <name>")
os.Exit(1)
} else {
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
err, vagueErr := lib.RemoveRepository(os.Args[3], common.DefaultRemoveRepositoryFromDB, logger)
if err != nil || vagueErr != nil {
common.RemoveRepositoryHandleError(err, vagueErr, logger)
}
}
case "list":
err := common.EstablishDBConnection(logger)
if err != nil {
fmt.Println(color.RedString("Failed to establish a connection to the database: " + err.Error()))
os.Exit(1)
}
repos, err := common.DefaultListRepositoriesInDB()
if err != nil {
fmt.Println(color.RedString("Failed to get repositories: " + err.Error()))
os.Exit(1)
}
if len(repos) == 0 {
fmt.Println("No repositories.")
} else {
fmt.Println("Repositories:")
for _, repo := range repos {
fmt.Println("\n" + repo.Name + ":\n" + " URL: " + repo.URL + "\n Owner: " + repo.Owner)
}
fmt.Println()
}
}
default:
fmt.Println(color.RedString("Unknown or unimplemented command: " + os.Args[1]))
os.Exit(1)
}
}